明明写了大量测试代码,却没抓住真正的 Bug,而且每修复一个功能,测试就接连失败过吗?
先说结论,优秀的单元测试不在于数量或覆盖率,而在于是否遵循“FIRST 原则”。FIRST 分别代表 Fast(快速)、Isolated(独立)、Repeatable(可重复)、Self-validating(自验证)和 Timely(及时)。
记住这五点,测试就不再是负担,而会成为可靠的安全网。今天结合我的经验,逐一说明。
FIRST 原则一览
FIRST 是 Robert Martin(Uncle Bob)在《Clean Code》一书中介绍后广为流传的原则。
每个字母都代表优秀单元测试的一项条件。
| 字母 | 含义 | 要点 |
|---|---|---|
| F | Fast | 数百个在几秒内完成 |
| I | Isolated | 测试之间互不干扰 |
| R | Repeatable | 随时随地运行都得到相同结果 |
| S | Self-validating | 自动判定通过或失败 |
| T | Timely | 在适当时机编写 |
下面逐一拆解。
Fast:为什么必须这么快?
单元测试必须足够快。真的。
因为速度慢,开发者就不会运行它们。一次要花 3 分钟的测试套件,最后往往只会在提交前勉强运行。
每次修改代码时,都能毫无负担地运行数百次,测试才算发挥作用。
变慢最常见的原因是访问真实数据库、网络和文件系统。通常应使用测试替身(Mock、Stub)替换这些外部依赖。
可以将目标设为每个测试几毫秒,全部数百个测试在几秒内完成。
Isolated:测试不能互相干扰的原因
每个测试都应该独立,不能依赖其他测试的结果。
例如,测试 A 创建的数据被测试 B 使用时,A 一旦失败,B 也会跟着崩溃。只要执行顺序改变,结果也会不同。
因此,每个测试都应该自行准备所需数据,并在结束后彻底清理。
下面是每次测试前初始化状态的常见模式。
struct CalculatorTests {
let calculator: Calculator
init() {
// 每次测试都会创建新实例 → 防止共享状态 suite
calculator = Calculator()
}
}
这样无论哪个测试先运行,都不会互相影响。
Repeatable & Self-validating:结果不能不稳定
先看 R,也就是可重复性。测试无论运行多少次、在哪种环境中运行,都应该得到相同结果。
今天通过、明天失败的测试称为 flaky 测试,是破坏信任的主要原因。
依赖当前时间或随机值很容易导致这类问题。如果必须处理时间,注入固定值进行控制会更安全。
S,即自验证,同样重要。测试必须自行判断通过还是失败。
把值打印到控制台后由人用眼睛确认,不算测试。应该使用 #expect 等断言自动判定结果。
优秀的测试只会显示两种结果:绿色或红色。如果让人思考“这样对吗?”,这个测试就已经失败了。
Timely:什么时候编写测试最合适?
最后的 T 代表及时性,也就是在适当时机编写测试。
在 TDD(Test-Driven Development,测试驱动开发)中,会先编写测试,再编写生产代码。即使不完全采用这种方式,关键也是在实现功能的相近时间一起编写测试。
如果功能完成几周后才想一次性补齐测试,代码往往已经固化成难以测试的结构。
不要把测试拖到以后,这就是 Timely 的核心。
总结
FIRST 与其说是需要背诵的规则,不如说是一份检查清单:当测试令人烦闷时,它能帮助你找出偏离之处。
速度慢吗?彼此耦合吗?结果不稳定吗?需要人工检查吗?是不是写得太晚?只要检查这五点,测试质量就会明显提升。
从今天开始,每写一个测试时都想一想 FIRST。它一定会成为可靠的伙伴。

