先从一个三百行的下单方法说起
上周朋友拉我看一个订单服务。一个方法三百多行,满屏 if。他说,单元测试根本没法写。我问,你想测什么。他说,测下单。我说,那不叫单元测试,那叫祈祷。
真的。单元测试的第一道坎,往往不是不会写断言,而是根本找不到一个可以独立运行的“单元”。业务代码把数据库、缓存、HTTP 调用、消息队列全揉在一起,你写出来的测试只会变成集成测试的穷人版。跑一次三十秒,失败原因还可能是测试环境少了字段。
所以别急着上 Mockito,也别急着骂 JUnit。先看方法边界。把计算折扣、判断库存、拼装订单号这些逻辑拆出来。拆到啥程度?一个函数只做一件事,给它输入,它给输出。能这么拆,单元测试才有活路。

别把覆盖率当成 KPI 挂墙上
很多团队一提高覆盖率,画风就变了。测试里全是对 getter 的调用,断言只有 assertNotNull。看起来很努力。但真出问题,一个都拦不住。
我见过更离谱的:为了让覆盖率数字好看,有人把测试写成“复读机”。方法返回 true,就断言 true。方法返回 false,就断言 false。可边界呢?异常呢?空集合呢?金额舍入呢?这些才是生产事故的常客。
单元测试的核心不是覆盖多少行,而是锁住那些你不希望改变的行为。比如:优惠券过期一天就不能用;金额 0.005 应该怎么舍入;用户 ID 为空时必须抛哪个异常。把这些写成测试,比一百个 getter 断言有用。
不过话说回来,覆盖率也不是完全没用。它像体检报告里的某个指标。高了不代表健康,低了要追问原因。把它当线索,别当奖杯。

让测试先失败一次再修代码

我有个习惯:写新逻辑之前,先写一个会失败的测试。很老派,对吧?但特别管用。
原因不神秘。人写代码时会不自觉地把假设塞进去。先写测试,等于先把需求钉在墙上。测试失败,说明你还没实现。测试通过,说明至少这个场景走通了。然后再补异常、补边界、补并发。
当然,不是所有场景都值得这么干。探索性原型、一次性脚本,硬套 TDD 就是浪费时间。可一旦进入长期维护的主干代码,尤其支付、权限、计价这类地方,测试先行能省掉后来无数个“谁把这里改了”的夜晚。
还有个细节:测试名别叫 test1、test2。写清楚场景和预期。比如 过期的优惠券不能抵扣订单金额。过半年再看,名字本身就是文档。
团队里怎么让单元测试活下来

个人写测试靠热情,团队写测试靠机制。机制不复杂,但得真实落地。
- 提交前跑本地测试。别等 CI 报红才发现。
- CI 里单独分一个单元测试阶段。慢的集成测试另放。别让一个挂掉的数据库拖死所有反馈。
- 代码评审看测试。不是看数量,是看有没有覆盖这次改动的边界和异常。
- 允许删测试。业务规则变了,旧测试就是负债。别舍不得。
说实话,单元测试从来不是银弹。它不能替代集成测试,也抓不住所有并发问题。但它能把最廉价、最频繁的那层验证做扎实。代码改完,几秒钟跑完几百个用例,心里有底。这个底气,比任何口号都值钱。
如果你们团队现在写不下去,别先换框架。先找一个最痛的方法,拆小,写三个测试:正常路径、边界值、异常输入。跑通。再复制这个节奏。别贪多。