第 8 章 · 软件测试

测试是「代码以后还会被改」这个事实的保险。本章澄清测试的分类学(别再混淆单元测试和集成测试)、测试金字塔(数量怎么配)、好测试的标准,以及 TDD 的真实面貌。测试写不好比不写更糟(假信心 + 维护负担),所以本章的另一半篇幅在讲「什么是不好的测试」。

8.1 测试的分类学:按规模分

层级范围快慢举例
单元测试一个类/函数,依赖全 mock毫秒级test: 负余额时 withdraw 抛异常
集成测试多模块协作,真数据库真消息队列秒级提交订单 → 查库确认落账
端到端(E2E)整系统,模拟真实用户分钟级Playwright 点页面完成下单

再加两个正交维度:手工 vs 自动(自动是复利,手工是一次性消耗)与黑盒 vs 白盒(从外部行为测 vs 知道内部结构测)。另一个常被混淆的是回归测试:不是一种技术,是一种用途(为修过的 bug 写测试,保证它不再复发;全部自动化测试在回归意义上都属于它)。

8.2 测试金字塔:数量怎么配

Mike Cohn 的金字塔:单元测试多而快(地基)、集成测试适中、E2E 少而稳(塔尖):

      /  E2E  \        少:慢、脆(页面结构一改就坏)
     / 集成测试 \       中:真实协作的关键路径
    /  单元测试  \      多:快、稳、定位精确

为什么倒过来(冰激凌反模式)不好?大量 E2E 测试跑一次半小时、还因为 UI 微调整批红,团队开始跳过测试,信心崩塌。分配的直觉:在金字塔的低层把行为钉死,高层只测「层级之间的连接」。数据库课第 7 章的事务、第 3 章的约束,在集成层测;业务规则在单元层测。

8.3 好测试的标准与写法

一个单元测试的标准结构 AAA:Arrange(准备数据与依赖)、Act(调用被测行为)、Assert(断言结果):

@Test
void withdraw_exceedingBalance_throws() {          // 测试名 = 方法_场景_期望
    Account a = new Account(100);                  // Arrange
    assertThrows(IllegalArgumentException.class,   // Act + Assert
        () -> a.withdraw(200));
}

好测试四条标准:快(毫秒级,慢了没人跑)、独立(测试之间无依赖、顺序无关,数据库课的事务回滚/每测清库是常用手段)、可重复(任何环境跑结果一致,不依赖网络和真实时间)、自解释(红了不用调试就知道哪坏了,一个测试一个断言主题)。

Mock(替身)的正确姿势:只 mock 你拥有的边界(自己的 Repository 接口,第 4 章的依赖倒置在这里兑现),不 mock 第三方库内部(那是集成测试的活);mock 太多 = 测试在测「实现细节」而不是行为,一重构全红(第 7 章),这是「烂测试比没测试糟」的最常见来源。

8.4 TDD:先写测试的真实面貌

TDD(测试驱动开发)的三步循环:红(写一个会失败的测试)→ 绿(用最简单的代码让它过)→ 重构(第 7 章,在保护网下整理)。它的真实价值不在「测试覆盖率」,而在:

  • 需求即时显形:写测试 = 把验收标准(第 3 章)翻译成可执行形式,含糊的需求当场写不出来;
  • 设计反馈:难写的测试暴露难用的 API(耦合太深、副作用乱窜),测试是第一位用户;
  • 持续保护网:每轮结束时代码处于「全绿」状态,重构随时可以开始。

诚实的提醒:TDD 全程严格推行在真实项目中少见,更常见的采纳是「核心业务逻辑用 TDD、胶水代码先写后补」。判据还是第 7 章的:这段代码以后会不会频繁被改?会,值得网。

8.5 覆盖率与测试策略

覆盖率(行覆盖/分支覆盖)是诊断工具不是目标:80% 覆盖率不保证质量(全是没有断言的 smoke 测试也能刷出来),但 30% 覆盖率一定说明保护网有大洞。合理用法:盯着「关键路径的分支覆盖」和「覆盖率的变化趋势」,别设成 KPI。

另一个实操层面:修 bug 必配回归测试(先写一个复现 bug 的测试,看着它红;修好看它绿;永远留在套件里)。这个流程把每个 bug 变成套件的一次加固,是「测试资产随时间增值」的关键机制。

常见错误

  • 测试断言软。assertNotNull(result) 之类永不失败的断言;断言要能区分「对」和「看起来对」。
  • 测试共享状态。静态变量、共享数据库不清理,测试顺序一换就炸(违反 8.3 独立性)。
  • 测实现不测行为。mock 了内部三个调用、断言了方法被调用的次序,一重构全崩;回到「这个行为对用户意味着什么」。
  • 慢测试进主干套件。套件超过十分钟,开发者开始跳过;快慢分层(第 10 章 CI 里快套件先跑)。
  • 跳过失败测试。@Disabled 一贴了事;失败测试要么修复要么删掉并记录,「挂着的绿灯」比没有绿灯更危险。

练习

  1. 动手:给一个现成的类(第 5 章的 BankAccount 或你自己的)写完整单元测试套件:正常路径、边界(0、负数、上限)、异常,至少 8 个测试,遵守 AAA 与命名法。
  2. 动手:体验 TDD:实现一个「购物车」(加商品、算总价、满 99 减 10),严格按红绿重构循环推进,记录每轮的测试先行的体感。
  3. 手算:套件 500 个测试,单元 450 个(每个 10ms)、集成 45 个(每个 2s)、E2E 5 个(每个 3min)。算总耗时;若 CI 要求 5 分钟内,你的分层怎么调?
  4. 思考:「100% 覆盖率」是否可能且是否有意义?用 8.5 的「断言质量」论证覆盖率上限,再想想「变异测试(mutation testing,故意改代码看测试抓不抓得到)」为什么比覆盖率更能测出「测试的真实强度」。