跳转到内容

测试

测试是对行为的规格说明。本章讲测试金字塔、测试替身、TDD 的取舍、属性测试与模糊测试,以及 flaky 测试的成因与治理。

层测什么速度数量
单元函数/类毫秒最多
集成模块组合(数据库、API)秒级中
端到端(E2E)完整用户路径分钟级最少

金字塔的依据是成本与反馈速度:单元测试快而便宜(改了立刻知道),E2E 慢而脆(环境、时序、UI 都是故障源)。反模式是"冰淇淋筒"(E2E 为主)——脆弱且慢,CI 排队几小时。

什么该测、测到什么程度的判断标准:

  • 测行为,不测实现:测试应该只看公开接口的输入输出——改内部实现不该改测试;
  • 分支覆盖是底线不是目标:覆盖率高不代表测得好(断言弱),但覆盖低的代码路径大概率有 bug;
  • bug 回归必须配测试:每个修过的 bug 留下一个复现它的测试——回归测试是"知识不蒸发"的机制。
  • stub:返回预制答案(查状态);
  • mock:验证交互(是否被调用、以什么参数调用);
  • fake:轻量可用的实现(内存数据库)。

mock 的纪律:mock 依赖的行为,别 mock 被测的实现细节——过度 mock 让测试与实现耦合(改实现就碎),且测试通过但行为错(mock 了错误假设)。集成测试用真依赖(测试容器、内存版)常比 mock 更有信息量——"测试替身越少越真实",mock 留给慢的/不可控的外部依赖。

测试驱动开发(红-绿-重构)的收益:

  • 设计压力:难写的测试暴露难用的接口——可测试性设计(依赖注入、小接口)是 TDD 的真正回报;
  • 快速反馈回路与行为规格(测试即文档)。

取舍:严格 TDD 对探索性代码过重(需求本身在变),对规格清晰的逻辑(解析器、算法)极好。务实的中间态:核心逻辑 TDD,胶水与 UI 集成测试补。

  • 属性测试(property-based,Hypothesis/QuickCheck):不给具体用例,给性质("反转两次等于原样"、"序列化后反序列化相等"、"输出是有序的")——框架随机生成用例并收缩(shrink)最小反例。属性测试能找到人想不到的边界(空串、极大值、Unicode、并发交错);
  • 模糊测试(fuzzing):随机/变异输入找崩溃与安全漏洞——覆盖引导的模糊测试(AFL、libFuzzer、OSS-Fuzz)是解析器与安全代码的标配(→ Web 安全)。
hypothesis.py
from hypothesis import given, strategies as st
@given(st.lists(st.integers()))
def test_sort_idempotent(xs):
assert sorted(sorted(xs)) == sorted(xs)
@given(st.lists(st.integers()))
def test_sort_preserves_elements(xs):
assert sorted(xs) == sorted(xs, reverse=True)[::-1]

flaky 测试(时好时坏)比没有测试更糟:它摧毁对 CI 的信任,"重跑就绿"成为习惯。

flaky 的成因通常是并发或时间依赖:

  • 并发:测试之间共享状态(全局单例、文件、数据库未清理);
  • 时间:依赖 sleep(用条件等待代替)、依赖"今天"(固定时钟);
  • 顺序:依赖执行顺序(测试应互相独立)。

治理:隔离(每个测试独立状态)、注入时钟、条件等待、 quarantine(隔离持续 flaky 的测试直到修复)。

  • 金字塔的依据是成本与反馈速度:单元多、集成中、E2E 少
  • 测试是对行为的规格说明,不是对实现的复述;bug 回归必配测试
  • mock 验证交互,stub 返回预制答案;替身越少越真实
  • 属性测试能找到人想不到的边界:性质 + 随机生成 + 最小反例收缩
  • flaky 通常是并发或时间依赖:隔离状态、注入时钟、条件等待