SuperAPI AI 工具集

unit-test

测试与质量

查看项目

更新于 11-02 16:14

技能介绍

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。