Unit Test
概述
单元测试在完全隔离的条件下验证一个单元(函数、类或模块)的行为。凡是跨架构边界的依赖,都用测试替身替换。速度和确定性不可妥协。
所需背景:先应用 auto-test 的通用原则;本技能补充的是单元范围的细节。
何时使用
为单个函数或类编写测试
审查单元测试是否存在隔离问题或测试气味
在单元层面决定 mocking 策略
诊断慢速、不稳定或脆弱的单元测试
不适用:有意使用真实数据库、真实 HTTP 或真实文件系统的测试——参见 integration-test。
核心原则
严格隔离
不使用真实 I/O:不发起网络调用、不访问数据库、不访问文件系统、不启动子进程。
不涉及真实时间:不使用 sleep,不读取系统挂钟时间。注入或伪造时钟。
只测试一个单元。如果你发现自己在装配两个真实协作者,那你写的是集成测试。
Mocking 理念
在架构边界处 mock,而不是对每个内部调用都 mock。
mock 的是单元所依赖的东西,而不是它由什么构成。
当假实现(in-memory 实现)写起来简单时,优先用假实现,而不是 mock(断言交互的桩)。
绝不断言内部辅助函数被调用的次数——那是在测试实现,而不是行为。
Fixture 设计
Fixture 只布置测试所需的东西,不多做。
共享 fixture 用于重复出现的布置模式,而不是用来隐藏意图。
避免 autouse 或全局生效的 fixture,除非该设置确实对所有测试通用(例如静默日志)。
单元测试中的 AAA
GOOD——各阶段清晰分离
def test_invoice_total_applies_discount_when_eligible(): # Arrange items = [Item(price=100), Item(price=50)] customer = Customer(tier="gold") # gold = 10% discount
# Act
total = calculate_invoice_total(items, customer)
# Assert
assert total == 135 # 150 - 10%
BAD——arrange 泄漏进 assert,魔法常量
def test_invoice(): assert calculate_invoice_total([Item(100), Item(50)], Customer("gold")) == 135
快速参考——单元测试气味清单
气味|症状|修复
过度 mock|mock 设置行数多于断言行数|只在边界 mock;测试真实逻辑
测试内部实现|断言私有方法的调用次数|断言输出或可观察状态
arrange 中出现构造逻辑|new Service(new Repo(new DB(...)))|使用工厂辅助函数或依赖注入
隐式 arrange|依赖模块级或类级状态|在测试体内显式声明所有状态
慢速单元测试|测试耗时超过约 50ms|查找隐藏的 I/O;注入假时钟
同义反复的断言|assert result == service.compute(x)|对照已知的期望值断言
多个行为|一个测试同时检查正常路径和错误路径|拆分为单独的参数化用例
参数化优于重复
当同一场景以不同输入重复出现时,应参数化而不是复制:
重复——难以扩展
def test_discount_gold(): ... def test_discount_silver(): ... def test_discount_none(): ...
参数化——增加一行即可扩展
@parametrize("tier,expected", [("gold", 135), ("silver", 142.50), ("none", 150)]) def test_discount_by_tier(tier, expected): ...
常见运行器(不指定框架)
语言|常见单元测试运行器
Python|pytest、unittest
JavaScript / TypeScript|Vitest、Jest
Java|JUnit 5、TestNG
.NET|xUnit、NUnit、MSTest
Go|内置 testing 包
Rust|内置 #[test]
上述原则适用于所有运行器。运行器特定写法请参考团队的编码规范或框架文档。
常见错误
mock 被测单元本身——如果 mock 了你正在测试的类,那你什么也没测。
测试之间共享可变状态——一个测试修改了模块级列表,下一个测试就被污染。
断言中使用真实时间——assert result.created_at == datetime.now() 总是在和时钟赛跑。
臃肿的 arrange 块——30 行准备代码意味着该单元依赖过多;应考虑拆分该单元,而不是增加更多 mock。