单元测试不该访问数据库,必须隔离外部依赖;应通过依赖注入和 mock 替换 db 调用;集成测试才覆盖 orm 行为、约束、事务等真实交互场景。

单元测试该不该访问数据库
不该。只要涉及数据库读写,就不再是单元测试,而是集成测试——这是最常被混淆的边界点。
单元测试的核心是隔离:用 unittest.mock.patch 或 pytest-mock 替换掉所有外部依赖(DB、http、文件系统)。一旦你看到 setUpTestData 里调用了 MyModel.objects.create(),或者测试里出现了 assert obj in MyModel.objects.all(),那它已经越界了。
- 真实场景中,django 的
TestCase默认开启事务并回滚,看起来“快”,但本质仍是集成测试——它依赖数据库 schema、迁移状态、甚至触发器逻辑 - postgresql 的序列值、mysql 的 AUTO_INCREMENT 行为,在不同测试顺序下可能不一致,导致偶发失败
- CI 环境若未预装对应数据库或权限不足,
TestCase会直接报OperationalError: could not connect to server
怎么给带数据库操作的函数写真正的单元测试
把数据访问逻辑抽成可注入的依赖,再在测试中传入模拟实现。
比如一个统计用户活跃度的函数:
立即学习“Python免费学习笔记(深入)”;
def calculate_user_score(user_id: int, db_client: DatabaseClient) -> float: data = db_client.fetch_raw_metrics(user_id) # ← 这个调用要能被替换 return sum(data) / len(data) if data else 0.0
测试时传入一个返回固定列表的 mock 对象:
- 用
unittest.mock.Mock(spec=DatabaseClient),避免拼写错误导致静默失败 - 显式设置
mock_db.fetch_raw_metrics.return_value = [10, 20, 15],而不是靠 side_effect 随机返回 - 不要 mock
DatabaseClient.__init__——它通常不做实际连接,mock 它反而掩盖构造逻辑问题 - 如果函数内部硬编码了
django.db.connection,说明它没做依赖解耦,得先重构,再测
集成测试该覆盖哪些东西
只覆盖那些“必须连起来跑才说得清对错”的路径:ORM 查询链、事务边界、外键约束、数据库函数(如 COALESCE、jsonb_extract_path)的实际行为。
典型例子:
- Django 的
select_related+prefetch_related组合是否真生成了预期 SQL?用assertNumQueries检查,不是靠 print - PostgreSQL 的
EXCLUDE using gist约束是否阻止了时间重叠插入?得真正执行save()并捕获IntegrityError - 使用
django.contrib.postgres.fields.JSONField时,Filter(data__tags__contains=["a"])是否返回正确结果?JSON 查询行为高度依赖后端驱动和版本 - 避免在集成测试里验证业务规则——比如“VIP 用户折扣不能超过 50%”,这属于单元测试范畴;集成测试只管“这个 discount 字段确实存进了 DB,并能被 WHERE 条件查出来”
为什么 pytest-django 的 –reuse-db 不稳定
因为它复用的是整个数据库实例,而多个测试模块可能并发修改同一张表的结构或数据,尤其当有人误用 makemigrations --empty 或手动执行 ALTER table 时。
常见现象包括:
-
django.core.exceptions.FieldError: Cannot resolve keyword 'xxx' into field—— 某个测试偷偷改了 model,但 migration 没提交,其他测试加载旧模型缓存 - PostgreSQL 报
relation "auth_user" does not exist——--reuse-db假设 DB 是干净的,但 CI 中前序 job 可能残留临时表 - sqlite 下看似正常,但切换到 PostgreSQL 后测试失败——因为 SQLite 忽略很多约束检查(如 CHECK、DEFERRABLE),掩盖了真实问题
- 更稳妥的做法是:本地开发用
--reuse-db加速,CI 中强制用--create-db,哪怕慢一点,也确保每次都是干净起点
边界从来不是靠命名区分的,而是看它是否引入了不可控变量。数据库、网络、时钟、随机数——只要有一个在动,你就已经不在单元测试的地盘上了。