浏览知识库目录

Python

测试、调试与日志

测试、调试与日志

测试保证修改没有破坏既有行为,日志帮助定位无法在本地复现的问题。本篇从模型、表单、视图测试讲到调试方法和生产日志。


一、测试什么

优先测试项目自己的业务规则,而不是验证 Django 框架本身:

  • 模型方法、约束和状态转换。
  • 表单自定义校验。
  • 认证、权限和对象归属。
  • 关键 CRUD 与重定向。
  • API 状态码和响应结构。
  • 容易回归的历史缺陷。

二、模型测试

notes/tests/test_models.py

from django.contrib.auth import get_user_model
from django.test import TestCase

from notes.models import Message

User = get_user_model()


class MessageModelTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            username="lin",
            password="test-password-123",
        )

    def test_string_representation_uses_name(self):
        message = Message(owner=self.user, name="第一条", content="正文")
        self.assertEqual(str(message), "第一条")

    def test_default_status_is_draft(self):
        message = Message.objects.create(
            owner=self.user,
            name="草稿",
            content="正文",
        )
        self.assertEqual(message.status, Message.Status.DRAFT)

TestCase 会为每个测试隔离数据库变化。测试之间不能依赖执行顺序,也不应共享上一个测试留下的数据。


三、表单测试

from django.test import SimpleTestCase

from notes.forms import MessageForm


class MessageFormTests(SimpleTestCase):
    def test_content_must_have_at_least_ten_characters(self):
        form = MessageForm(data={
            "name": "测试",
            "content": "太短",
            "status": "draft",
        })

        self.assertFalse(form.is_valid())
        self.assertIn("content", form.errors)

不访问数据库的测试可使用 SimpleTestCase,通常更轻量。


四、视图与权限测试

from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse

from notes.models import Message

User = get_user_model()


class MessageViewTests(TestCase):
    def setUp(self):
        self.owner = User.objects.create_user("owner", password="pass-12345")
        self.other = User.objects.create_user("other", password="pass-12345")
        self.message = Message.objects.create(
            owner=self.owner,
            name="私有留言",
            content="这是一条测试留言内容",
        )

    def test_anonymous_user_is_redirected_from_update(self):
        response = self.client.get(
            reverse("notes:update", kwargs={"pk": self.message.pk})
        )
        self.assertEqual(response.status_code, 302)

    def test_other_user_cannot_update_message(self):
        self.client.force_login(self.other)
        response = self.client.get(
            reverse("notes:update", kwargs={"pk": self.message.pk})
        )
        self.assertEqual(response.status_code, 404)

    def test_owner_can_update_message(self):
        self.client.force_login(self.owner)
        response = self.client.post(
            reverse("notes:update", kwargs={"pk": self.message.pk}),
            {
                "name": "已修改",
                "content": "修改后的留言内容足够长",
                "status": "draft",
            },
        )
        self.assertRedirects(
            response,
            reverse("notes:detail", kwargs={"pk": self.message.pk}),
        )
        self.message.refresh_from_db()
        self.assertEqual(self.message.name, "已修改")

权限测试至少包含匿名用户、无权用户和有权用户三类情况。


五、测试查询数量

def test_message_list_has_no_n_plus_one(self):
    Message.objects.bulk_create([
        Message(owner=self.owner, name=f"留言 {i}", content="测试内容")
        for i in range(10)
    ])

    with self.assertNumQueries(3):
        response = self.client.get(reverse("notes:list"))
        self.assertEqual(response.status_code, 200)

精确查询数会随认证、分页和模板变化,应在真正关心 N+1 回归的页面使用,不必给所有测试增加脆弱断言。


六、运行测试

# 全部测试
python manage.py test

# 某个应用
python manage.py test notes

# 某个测试类
python manage.py test notes.tests.test_views.MessageViewTests

项目增大后可引入 pytestpytest-django 和工厂库,但先理解 Django 自带测试工具,避免工具掩盖基础概念。


七、调试思路

遇到异常时按以下顺序:

  1. 阅读异常类型和堆栈最后几层。
  2. 找到第一个属于自己项目的文件与行号。
  3. 检查输入数据、对象状态和实际执行路径。
  4. 缩小到最小复现步骤。
  5. 为缺陷补一个失败测试,再修复代码。

开发环境可以暂停并检查现场:

breakpoint()

常用诊断命令:

python manage.py check
python manage.py showmigrations
python manage.py shell

不要在生产响应中显示调试页面,也不要把完整请求体、密码和令牌打印出来。


八、日志配置

settings.py

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "verbose": {
            "format": "{asctime} {levelname} {name} {message}",
            "style": "{",
        },
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "verbose",
        },
    },
    "loggers": {
        "django": {
            "handlers": ["console"],
            "level": "INFO",
            "propagate": False,
        },
        "notes": {
            "handlers": ["console"],
            "level": "INFO",
            "propagate": False,
        },
    },
}

业务代码:

import logging

logger = logging.getLogger(__name__)

logger.info("message_published", extra={"message_id": message.pk})

生产环境推荐结构化日志,至少包含时间、级别、模块、请求 ID、用户 ID(如适用)和关键对象 ID。不要记录密码、会话 Cookie、银行卡、完整令牌或不必要的个人信息。


九、本篇检查清单

  • 能测试模型、表单、视图和权限。
  • 测试之间相互独立,不依赖执行顺序。
  • 能用失败测试复现缺陷。
  • 会从异常堆栈定位自己项目中的第一处问题。
  • 日志包含排查所需上下文,但不泄露敏感信息。

上一篇:配置、中间件、信号与管理命令 | 下一篇:使用 DRF 开发 RESTful API