测试
测试是对行为的规格说明。本章讲测试金字塔、测试替身、TDD 的取舍、属性测试与模糊测试,以及 flaky 测试的成因与治理。
| 层 | 测什么 | 速度 | 数量 |
|---|---|---|---|
| 单元 | 函数/类 | 毫秒 | 最多 |
| 集成 | 模块组合(数据库、API) | 秒级 | 中 |
| 端到端(E2E) | 完整用户路径 | 分钟级 | 最少 |
金字塔的依据是成本与反馈速度:单元测试快而便宜(改了立刻知道),E2E 慢而脆(环境、时序、UI 都是故障源)。反模式是"冰淇淋筒"(E2E 为主)——脆弱且慢,CI 排队几小时。
什么该测、测到什么程度的判断标准:
- 测行为,不测实现:测试应该只看公开接口的输入输出——改内部实现不该改测试;
- 分支覆盖是底线不是目标:覆盖率高不代表测得好(断言弱),但覆盖低的代码路径大概率有 bug;
- bug 回归必须配测试:每个修过的 bug 留下一个复现它的测试——回归测试是"知识不蒸发"的机制。
- stub:返回预制答案(查状态);
- mock:验证交互(是否被调用、以什么参数调用);
- fake:轻量可用的实现(内存数据库)。
mock 的纪律:mock 依赖的行为,别 mock 被测的实现细节——过度 mock 让测试与实现耦合(改实现就碎),且测试通过但行为错(mock 了错误假设)。集成测试用真依赖(测试容器、内存版)常比 mock 更有信息量——"测试替身越少越真实",mock 留给慢的/不可控的外部依赖。
TDD 的取舍
Section titled “TDD 的取舍”测试驱动开发(红-绿-重构)的收益:
- 设计压力:难写的测试暴露难用的接口——可测试性设计(依赖注入、小接口)是 TDD 的真正回报;
- 快速反馈回路与行为规格(测试即文档)。
取舍:严格 TDD 对探索性代码过重(需求本身在变),对规格清晰的逻辑(解析器、算法)极好。务实的中间态:核心逻辑 TDD,胶水与 UI 集成测试补。
属性测试与模糊测试
Section titled “属性测试与模糊测试”- 属性测试(property-based,Hypothesis/QuickCheck):不给具体用例,给性质("反转两次等于原样"、"序列化后反序列化相等"、"输出是有序的")——框架随机生成用例并收缩(shrink)最小反例。属性测试能找到人想不到的边界(空串、极大值、Unicode、并发交错);
- 模糊测试(fuzzing):随机/变异输入找崩溃与安全漏洞——覆盖引导的模糊测试(AFL、libFuzzer、OSS-Fuzz)是解析器与安全代码的标配(→ Web 安全)。
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 测试
Section titled “flaky 测试”flaky 测试(时好时坏)比没有测试更糟:它摧毁对 CI 的信任,"重跑就绿"成为习惯。
flaky 的成因通常是并发或时间依赖:
- 并发:测试之间共享状态(全局单例、文件、数据库未清理);
- 时间:依赖
sleep(用条件等待代替)、依赖"今天"(固定时钟); - 顺序:依赖执行顺序(测试应互相独立)。
治理:隔离(每个测试独立状态)、注入时钟、条件等待、 quarantine(隔离持续 flaky 的测试直到修复)。
- 金字塔的依据是成本与反馈速度:单元多、集成中、E2E 少
- 测试是对行为的规格说明,不是对实现的复述;bug 回归必配测试
- mock 验证交互,stub 返回预制答案;替身越少越真实
- 属性测试能找到人想不到的边界:性质 + 随机生成 + 最小反例收缩
- flaky 通常是并发或时间依赖:隔离状态、注入时钟、条件等待