单元测试覆盖率提升方法的一线踩坑实录
上周帮朋友团队救场,刚打开他们项目的测试报告,我人直接傻了。27%。整整三个月迭代攒下来的核心交易链路,一半以上的代码连个最基础的断言都没有。朋友挠头说,大家都忙需求,哪有空写测试啊,覆盖率都是最后冲刺阶段凑数的——随便抓几个纯getter的方法写两行,数字是好看了,真出问题的时候该炸还是炸。
别上来就凑覆盖率数字
我见过太多团队的覆盖率提升路径,从一开始就走歪了。为了赶季度末的指标,大家抱着“能凑一行是一行”的心态,把所有实体类的getter、setter方法全部补上测试用例,看着报表上的数字从30%跳到80%,全组人开香槟庆祝。结果上线第一周,支付回调的核心分支漏测,三笔订单因为逻辑判断错误重复扣款,算下来直接损失近两千元。
那时候大家才反应过来,我们追了半天的数字,根本没触碰到真正有价值的覆盖范围。高风险分支的实际覆盖,从来不是靠堆无意义的用例堆出来的——金额计算、权限校验、异常分支这些代码,哪怕总行数只占项目的10%,每一行覆盖到了,比覆盖1000行重复的表单校验逻辑有用一百倍。那些凑数的用例,除了让测试报告好看点,半毛钱的实际防护作用都没有。
从最容易出问题的地方拆测试
很多人写测试的习惯,是对着整个类从头到尾跑一遍,能走到哪算哪。这种写测试的方式,写得慢,覆盖率涨得虚,还容易漏关键分支。我现在带团队写测试,从来不让大家上来就写全量路径,先找代码里最容易出问题的几个点位:所有涉及金额计算的方法、所有抛业务异常的分支、所有依赖外部接口的回调逻辑。
举个最常见的订单状态流转的例子,之前的测试逻辑是直接把整个流转方法跑一遍,断言最终的订单状态。但实际上这段逻辑里藏着三个核心分支:用户主动取消、超时自动取消、商家拒单,每个分支下面还有嵌套判断:已支付的订单取消要触发退款流程,未支付的直接关单,已发货的取消要先做物流拦截。之前的测试只覆盖了未支付直接取消的最简单路径,剩下两个分支全漏。后来补测试的时候,我们也不写那种大而全的集成测试,就把每个分支的输入拆出来:传已支付的订单ID调用取消接口,断言退款流水正常生成;传已发货的订单ID调用取消接口,断言返回物流拦截提示。这些用例写下来,可能总共只占代码总行数的15%,覆盖率数字涨得慢,但真出问题的概率掉得快。

这里有个我们用了快两年的小技巧:每次code review的时候,先把新提交代码里的分支条件标出来,每个if/else的反向分支必须写测试。太多人写测试只测正常走通的路径,比如碰到if (amount > 0) 就传个100进去测,从来不会传个-1,传个null,传个超出阈值的超大数进去测异常分支。这些反向分支的测试写下来,覆盖率可能涨个10%都不到,但线上的低级bug能少一半。
别把写测试变成额外的负担
我特别理解大家抵触写单元测试的情绪——本来需求排期就卡得死,还要单独抽时间补测试,换谁都烦。我们团队一开始硬推“每个需求必须带单元测试”的规则的时候,大家怨声载道,有人算过,写测试的时间比写业务代码的时间还长。后来我们把规则改了,效果反而好很多:不要为了补历史代码的测试单独排期,就跟着需求迭代走。每次改老代码的时候,顺手把当前这个方法的测试补上,算在这个需求的工作量里,不额外加任务。比如这次要改订单超时自动取消的逻辑,那就刚好趁这个机会把这个方法之前漏的三个分支的测试写了,不用单独拉个“覆盖率提升专项”的项目折腾大家。
还有个能省一半时间的工具技巧,直接用IDE自带的覆盖率插件,跑测试的时候直接把没覆盖到的代码行标红,不用自己对着十几页的HTML报告翻找哪块没写到。之前我们找未覆盖的代码要花半小时,现在插件直接在代码行旁边标上颜色,写测试的时候一眼就能看到哪行还没走到。
当然也别搞极端,我们从来不会强制要求所有代码100%覆盖。那种纯静态的页面渲染逻辑、写死的配置常量、没有任何分支判断的工具方法,硬要写测试纯粹是浪费时间。后来我们定的内部规则很宽松:核心业务逻辑覆盖率必须到90%以上,工具类、配置类代码覆盖率到50%就行,纯UI组件的测试不做强制要求。
说实话,我见过太多团队把覆盖率搞成了数字游戏。从20%冲到90%看着很厉害,实则一半的用例都是凑数的。写单元测试本质上不是为了给领导看报表,是为了下次改代码的时候,知道自己动的地方不会把之前的逻辑搞崩。哦对,还有个不能破的规矩:没有断言的测试用例直接打回,不管你跑了多少行代码——之前有个实习生刚进来,写的测试全是把方法跑一遍不报错就算过,这种用例写得再多,半毛钱用都没有。
就这么简单。不用搞什么高大上的方法论,把凑数的用例删了,把核心分支的测试补上,别把写测试当额外的任务,覆盖率自然就上来了。关键是,这个数字是真的能帮你挡线上bug的,不是给别人看的花架子。