单元测试的边界与收益
「测试覆盖率 80%」是个虚荣指标。单元测试的价值取决于你测的是什么的什么。
什么值得测
纯逻辑与业务规则:价格计算、状态流转、校验规则、日期处理。输入输出明确,断言稳定,回归价值最高。
曾经出过 bug 的地方:每个线上问题修复后补一个回归测试,是最诚实的测试用例来源。
对外契约:序列化/反序列化、API 响应结构、消息格式——破坏它们的影响远超单点逻辑。
什么不值得测
- getter/setter、纯转发的胶水代码:测了只是把实现复述一遍;
- UI 像素级布局:交给人眼看快照,单元测试得不偿失;
- 重度依赖外部状态的代码:mock 一大堆之后,测试断言的其实是 mock 本身。这类代码的正确策略是重构它,让可测的部分变多。
一个现实的结构
教科书金字塔(大量单测 + 少量集成 + 极少 E2E)在业务系统里的现实版:
E2E ← 关键路径 3-5 条,守住「用户能下单」这类底线
集成 ← 数据库真实读写、API 层,验证拼装正确
单元 ← 业务规则、工具函数,跑得最快、数量最多
集成测试的比重通常比教科书高——因为「各模块都对,拼起来错」才是业务 bug 的主产区。
好测试的形状
public function test_expired_coupon_is_rejected(): void
{
$coupon = new Coupon(discount: 10, expiresAt: new DateTimeImmutable('-1 day'));
$this->expectException(InvalidCouponException::class);
$cart->apply($coupon);
}
- 测试名描述行为,不是方法名;
- 一个测试一个断言点,失败时立刻知道是什么坏了;
- 断言结果,不断言实现(mock 调用次数只在必要时用);
- 跑得快。整套单测超过 30 秒,开发者就会开始跳过它。
何时引入
个人项目、原型期可以不写;一旦进入「多人维护 + 线上运行」状态,为业务规则写测试的回报率是所有工程实践里最高的之一。判断时机很简单:当你改一处逻辑、心里开始担心别处会不会坏的时候,就该补测试了。