本节摘要:测试代码是写给未来改代码的人的保险单,它自己也需要规范:命名让人不看实现就知道测什么,结构遵守准备执行断言三段式,断言精确到行为而非实现细节,测试彼此独立可乱序执行,测试与生产代码同等纳入评审。本节同时讨论覆盖率目标的合理设定与测试金字塔的层次分工。
阅读完本节,你应当能够:
一个团队的测试套件出了名地"脆":任何重构——哪怕只是提取一个函数——都让二十个测试变红;有的测试连数据库,跑一次八分钟;测试之间还有隐藏的顺序依赖,全量跑能过,单跑某个就挂(它依赖前一个测试留下的数据)。结果:测试失败不再意味着缺陷,而意味着"又要修测试了"。开发者看到红色第一反应是跳过而不是排查——当测试不可信,等于没有测试,甚至比没有更糟,因为它还在消耗维护工时。
这份脆性的根源是测试代码没按规范写:断言写到了实现细节上(改个内部结构就红)、测试之间共享状态(牵一发动全身)、命名含混(test1 到 test47,红了一个不知道坏了什么)。测试代码的第一性要明确:它不是一次性验证脚本,是要被维护多年的代码——生产代码每次改动它都要跟着跑,生产重构时它要能不改而全绿(这是重构安全的定义)。所以测试需要与生产代码同级的规范,外加几条测试特有的纪律。
测试名是失败时的第一行诊断信息。规范格式是"被测对象加场景加预期"三段式:
# 差 失败了不知道坏了什么 test_1() test_order_ok() # 好 失败信息自成一句诊断 test_calculate_total_applies_member_discount() test_withdraw_raises_when_balance_insufficient() test_fetch_user_returns_none_when_id_not_found()
好的测试名读起来是一句业务断言:"计算总价时应应用会员折扣"。测试列表因此成为模块行为的目录——新人想知道某模块支持哪些行为,读测试名列表比读代码快得多。
每个测试体遵守准备、执行、断言三段:准备测试数据与环境,执行被测行为,断言结果。三段之间空行分隔,每段尽量短。三段式的价值在失败排查:断言失败时,读者扫一眼准备段就知道前提,扫一眼执行段就知道操作,定位是几何级的快。一个测试只测一个行为——多行为混测的测试失败时,你要先弄清是哪个行为坏了。
这是最重要也最常违反的纪律。行为是外部可观察的契约(输入什么、输出什么、抛什么);实现是内部怎么做的。断言应只针对行为:
# 测实现 脆弱:把排序算法换成另一种就红 但行为没变 assert result.was_sorted_by_quick_sort() # 测行为 稳定:只关心结果有序 assert result == [1, 2, 3, 5]
测实现的测试会把每一次合理重构都误报为破坏,逼开发者"改实现必改测试",测试从保险单退化成枷锁。检验句式:不改变外部行为的前提下重写实现,测试应该全绿。
每个测试自备数据、自建环境、自己清理,不依赖其他测试留下的状态,不依赖执行顺序。共享状态的测试是定时炸弹:单独跑挂、批量跑过(或反过来),排查顺序依赖是测试世界里最浪费时间的活动之一。需要昂贵公共资源的场景(数据库),用每个测试独立事务或测试夹具按测试粒度提供,而不是全局共享一份可变数据。
测试代码与生产代码同库同评审。评审测试时问三句:失败时名字能说明坏了什么吗;断言的是行为还是实现;这个测试依赖别人吗。草率的测试比没有测试更危险——它制造"已验证"的错觉。
设定覆盖率目标(如核心业务逻辑百分之八十)有动员价值,但要清楚它的局限:覆盖率只说明"代码被执行过",不说明"行为被验证过"——没有断言的测试也贡献覆盖率。把覆盖率当体检指标(长期趋势)而不是考核指标(冲刺数字),否则会收获一批没断言的空跑测试。真正要盯的是核心路径覆盖与变更覆盖(新代码必须带测试)。
测试分三层,数量呈金字塔:单元测试(量大、快、测单个函数与类,毫秒级,是日常开发的秒级反馈)打底;集成测试(中量,测模块间协作与真实依赖交互)居中;端到端测试(少量,从外部视角走完整业务流,慢而脆,只覆盖最关键链路)封顶。常见失误是倒金字塔——大量端到端测试撑起"安全感",每次跑半小时、随机挂两个,团队逐渐习惯性忽略失败。分层原则:能在下层验证的不拖到上层。
| 层次 | 数量 | 速度 | 验证对象 | 失败定位 |
|---|---|---|---|---|
| 单元测试 | 多 | 毫秒级 | 单个函数或类 | 秒级定位 |
| 集成测试 | 中 | 秒级 | 模块协作 | 分钟级 |
| 端到端 | 少 | 分钟级 | 关键业务链路 | 小时级且脆 |
⚠️ 常见坑:为了凑覆盖率给 getter、setter、简单赋值函数写测试。这类测试贡献覆盖率数字却不拦缺陷,还稀释测试套件的信噪比。测试要瞄准有分支、有边界、有业务语义的逻辑——条件边界(空列表、零、负数、恰好越界)才是缺陷藏身处。
💡 关键直觉:写每个测试前默念它的完整生命周期——它将在未来几年里被执行上万次、在生产代码每次改动时被跑一遍、在某个深夜失败给你看。为这个生命周期写代码,命名、独立性、行为断言的规范就不再是束缚而是仁慈。
下一节是团队协作的最后一环:版本控制——把每次改动写成未来的排查档案。