不写单测省下的时间,迟早要加倍还回来

上周五下午,大刘改了一个计费函数。

改动很小,十几行,折扣的取整方式从四舍五入改成向下。他盯着看了一遍,逻辑清楚,边界也想到了,提交,等流水线绿了就上线。

当天晚上十一点,客诉炸了。

优惠金额和页面上显示的对不上,每单差一毛钱。一毛钱不多,可订单是按千算的。运营连夜统计,受影响的订单两千三百多笔。

回滚的回滚,写修复脚本的写脚本,给客户挨个补偿的挨个打电话。大刘折腾到凌晨四点。

第二天的复盘会上,leader 问他:改这个函数之前,你跑过原有的测试用例吗?

大刘愣了一下:这个模块没有用例。写了三年,一行测试都没有。

会议室安静了几秒。因为在场每个人心里都清楚,自己手上的老模块,大概率也一样。

一、省下来的时间,都记在暗账上

拒绝单测的理由,翻来覆去就那几个:忙,排期紧,需求都做不完。

这些理由看起来都成立。写实现两小时,写测试再来两小时,工期直接翻倍,哪个项目经理会批?

但这笔账漏了一块。

你省下的,是写测试的那两个小时。你没省下的,是之后每一次改动的回归成本。

改一行代码,你就得把主流程手动点一遍。登录、下单、支付、退款,挨个走。点到一半被人打断,回头重点。点漏一个分支,就是一次线上事故。

这些时间特别零散,不出现在任何排期表上,所以没人心疼。可它们加起来的总数,早就超过了当初写测试的成本。

有人真算过这笔账。

微软和 IBM 的几个团队在真实项目里做过对照研究。测试先行的那些团队,缺陷密度降了四成到九成,花的开发时间多了两到三成。

时间确实多花了。换回来的是,线上 bug 少了一个量级。

线上一个故障的代价,大家心里都有数:排查几小时,修复几小时,写事故报告再来几小时。要是赶上资损,那就不只是时间的问题了。

写单测的时间,是从排期里出的。不写单测的时间,是从事故里出的。

前者明码标价,后者利息惊人。

不写单测省下的时间,迟早要加倍还回来

二、单测买的,是改代码的底气

谷歌出了本黄皮书,《Software Engineering at Google》,专门拿出一章讲测试。

书里有个观点,我记到现在:测试最大的收益,是让修改变得便宜。

老代码为什么没人敢动?改动本身可能只要十分钟,验证这个改动没弄坏别的东西,要三天。测试把验证从三天压到三分钟,老代码才敢碰。

谷歌内部每天跑的自动化测试,是按亿次算的。这么大的体量,靠人手动回归,人早就疯了。

举个身边的例子对比一下。

两个模块,功能差不多。一个有三百个单测,一个零个。现在都要加一个新字段。

有测试的那个,改完,本地跑一遍,三分钟全绿,提测,下班。

没测试的那个,改完,先愣一会儿。这个字段别的页面用到没有?缓存刷新会不会有影响?想不出个头绪,把能想到的页面全手动点一遍,点完心里还是没底,提测备注栏写满“麻烦重点回归”。

一样的需求,一个三分钟收工,一个提心吊胆。

单测就是给代码上了一份保险。平时看着没用,出事的时候才知道值多少钱。

三、什么该测,什么不该测

很多团队一开始热情高涨,什么都测,两周后集体劝退。方向错了。

两个原则,能避开大部分坑。

第一个,测行为,别测实现。

用户买一件商品,会员打八折,订单金额算得对,这是行为,值得测。函数内部先算折扣再算运费,还是先算运费再算折扣,这是实现细节。明天一重构就变了,盯着它测,等于给自己上锁。

第二个,按金字塔分配。

测试金字塔这个说法,是 Mike Cohn 提出来的,Martin Fowler 后来专门写文章讲过。

塔基是单元测试,数量最多,毫秒级跑完,跑一万次也不心疼。塔身是集成测试,验证模块之间的配合,数量少一些。塔尖是端到端测试,模拟真实用户从头走到尾,最有说服力,也最慢、最贵、最脆。

最常见的错误,是把金字塔倒过来。端到端用例几百条,全量跑一次四十分钟,随机挂,还没法排查。跑的人越来越少,最后整套测试成了摆设,机器一红,大家的第一反应是“再跑一次试试”。

还有一个新手坑:给 gettersetter 写测试。行覆盖率蹭蹭上涨,价值为零。

覆盖率是写测试的结果,别反过来把它当目标去凑。为了数字好看而写的测试,测不出任何真问题。

测得对,永远比测得多值钱。

不写单测省下的时间,迟早要加倍还回来

四、从零开始,三步就够

道理讲完了,落到执行上,不需要什么轰轰烈烈的改革。三步,今天就能做。

第一步,新代码配一个测试。

不用一步到位,更不用追求覆盖率。今天写的函数,配一个最简单的用例:给个正常输入,断言输出。十分钟的事。

从此这个函数有了底线。谁改坏了它,机器会第一时间喊出来,用不着等用户来喊。

第二步,修 bug 之前,先写复现。

出了 bug,先别急着动手改。先写一个测试把它复现出来,让它红着。然后去修,修到它变绿。

这个测试从此留在那里,站岗。同一类 bug 再想偷偷溜回来,门都没有。

第三步,让测试自己跑。

测试慢,人就懒得跑,这是人性。把测试挂到提交流水线上,每次提交自动执行,红了就不让合。本地跑不完没关系,机器替你跑。

三件事坚持一两个月,变化会自己找上门:改代码的时候,手不抖了。

最后,从第一个测试开始写

回到大刘。

复盘之后,他们组立了两条规矩:新改动必须带测试,老代码不强求;修 bug 必须先写复现,再动实现。

半年后他跟我说,组里手工回归砍掉了大半,提测打回率反而降了。最有意思的是,新人上手速度快了很多。看一遍测试用例,就知道模块该怎么用。

JUnit 之父 Kent Beck 说过一句话,我一直很喜欢:我不是什么伟大的程序员,我只是个有着好习惯的普通程序员。

单测就是这么个好习惯。不用等团队推动,不用等领导批准。

今天打开你最熟的那个模块,挑一个函数,写下第一个测试。

写代码是给用户用的,测试是给自己写的。

后者让你睡得着觉。

分享到: 文章二维码
© 版权声明

暂无评论

您必须登录才能参与评论!
暂无评论...