9.1 测试框架与单元测试 本节摘要:Django 的测试体系构建于标准库测试框架之上,补齐了数据库隔离、测试客户端与夹具三块 Web 特有的拼图。本节讲测试类的结构、运行时发生了什么,然后为墨迹博客的模型与管理器写第一批单元测试。单元测试的对象是"逻辑单元",不碰网络、不碰浏览器,跑得飞快。 测试运行时发生了什么 一条命令: 运行器做的事:在应用里查找约定的测试模块与测试类,创建一套独立的测试数据库(默认在内存里),执行全部用例,然后销毁它。开发库的数据一动不动。更细一层:每条用例执行前,数据库事务被包起来,用例结束即回滚——用例之间互不知晓、互不污染。这个隔离设计解决了测试领域最恼人的"顺序依赖"问题,也让"造数据"成为每条用例自己的事。 输出里句号代表通过、F 代表失败、E 代表错误;
本节摘要:Django 的测试体系构建于标准库测试框架之上,补齐了数据库隔离、测试客户端与夹具三块 Web 特有的拼图。本节讲测试类的结构、运行时发生了什么,然后为墨迹博客的模型与管理器写第一批单元测试。单元测试的对象是"逻辑单元",不碰网络、不碰浏览器,跑得飞快。
一条命令:
python manage.py test
运行器做的事:在应用里查找约定的测试模块与测试类,创建一套独立的测试数据库(默认在内存里),执行全部用例,然后销毁它。开发库的数据一动不动。更细一层:每条用例执行前,数据库事务被包起来,用例结束即回滚——用例之间互不知晓、互不污染。这个隔离设计解决了测试领域最恼人的"顺序依赖"问题,也让"造数据"成为每条用例自己的事。
输出里句号代表通过、F 代表失败、E 代表错误;失败时断言差异一目了然。加上用例名后缀可以只跑一个用例,这在排查时比全量快一个量级。
from django.test import TestCase from django.contrib.auth.models import User from .models import Article, Category class ArticleModelTest(TestCase): def setUp(self): """每条用例前执行,造本类共用的初始数据。""" self.user = User.objects.create_user("tester") self.article = Article.objects.create( title="测试文章", body="内容", author=self.user) def test_str_returns_title(self): self.assertEqual(str(self.article), "测试文章") def test_default_status_is_draft(self): self.assertEqual(self.article.status, Article.DRAFT)
约定即知识:方法名必须以 test 开头才被发现;setUp 造公共数据;断言用 self.assert 系列,失败信息比裸断言友好得多。上例虽小,验证的却是第 2 章的两条设计决定(字符串表示与默认状态)。
模型层是单元测试的主场。测默认管理器的过滤语义:
class PublishedManagerTest(TestCase): def setUp(self): user = User.objects.create_user("u") Article.objects.create(title="草稿", body="x", author=user) Article.objects.create( title="已发布", body="x", author=user, status=Article.PUBLISHED) def test_published_excludes_draft(self): titles = list(Article.published.values_list("title", flat=True)) self.assertEqual(titles, ["已发布"]) def test_published_ordered_by_created_desc(self): qs = Article.published.all() self.assertEqual( list(qs), sorted(qs, key=lambda a: a.created_at, reverse=True))
这段用例守护的是第 2 章那个"过滤逻辑放进模型层"的决定——将来有人误改管理器,测试先红。测唯一约束用 assertRaises:
from django.db import IntegrityError class ConstraintTest(TestCase): def test_same_author_same_title_rejected(self): user = User.objects.create_user("u") Article.objects.create(title="重复", body="x", author=user) with self.assertRaises(IntegrityError): Article.objects.create(title="重复", body="y", author=user)
它验证的是数据库层的兜底——第 2 章说"并发下只有数据库约束能兜底",这里就是证据。
第 6 章的校验链是回归测试的重点对象:
from .forms import CommentForm class CommentFormTest(TestCase): def test_forbidden_words_rejected(self): form = CommentForm(data={"reviewer": "a", "content": "这是广告"}) self.assertFalse(form.is_valid()) self.assertIn("content", form.errors) def test_normal_comment_passes(self): form = CommentForm(data={"reviewer": "a", "content": "写得不错"}) self.assertTrue(form.is_valid())
直接构造绑定表单断言校验结果,不需要任何 HTTP 参与。正面与反例成对出现是表单测试的基本功:只测"坏数据被拒"不测"好数据能过",某天校验器写死成一律拒绝,测试照样全绿。

💡 一个反直觉的经验:测试写得越勤,单条测试越短。当 setUp 造数逻辑开始膨胀,往往说明被测代码本身该拆了——测试是设计的一面镜子。
再谈一个新手最常问的问题:测试到底要写多少才算够?给出可操作的答案比给出百分比更有用。墨迹博客的补测顺序是这样的:先测"坏了会出事故"的部分——模型约束、状态过滤、权限判断,这些是发布闸门真正依赖的防线;再测"改得最频繁"的部分——首页列表与文章详情这两条主干路径;至于字符串表示这类琐碎行为,顺手写一条即可,不值得专门投入。判断标准始终是业务后果而不是覆盖率数字:一条测试的价值等于它拦截的故障乘以该故障的发生概率,先堵后果重的,再堵概率高的。覆盖率工具的合理用法是拿它找"从未被测过的盲区",而不是刷一个好看的百分比。
写单元测试的过程里藏着一个高效的副产品:它是检验设计质量的自动仪器。给某段代码写测试费劲时——要造一大堆不相关的对象、要模拟掉五六个外部依赖、断言要翻山越岭地取值——别急着怪测试难写,这几乎总是设计问题的信号:职责过多、依赖过重、接口不清。墨迹博客重构期有个真实案例:文章状态流转的测试写了八十行还不稳定,深挖后发现是状态判断逻辑散落在模型、管理器、视图三处,测试被迫同时装配三层。把状态机收拢进模型的一个方法后,测试缩到十二行且再没闪失过。所以团队后来立了一条不成文的规矩:新代码的测试写不顺,先回头改设计,而不是硬着头皮把测试焊上去。测试驱动开发的深层价值不在覆盖率,在这面镜子照出的结构问题。
单元合格只是地基,下一节把视图、请求、跳转整条链路测起来。