上周去朋友的创业公司蹭咖啡。
刚好撞见后端组的小周把键盘往桌上一砸——三个月前他写的那套优惠券阶梯计算逻辑,这次618前临时改了个满减门槛,结果所有历史留存的用户优惠券金额全算错,客诉后台从下午两点到凌晨三点,刷出来的消息快把系统压垮。
没人敢担责。
毕竟那堆代码写的时候,连半行测试都没有,谁也说不清哪个判断分支藏着没被发现的边界bug。
单元测试真不是写来凑覆盖率的
我见过太多团队把单元测试搞成了形式主义。
为了过部门定下的覆盖率指标,大家挖空心思凑数:给实体类的getter、setter方法写测试,给toString方法写测试,甚至给空的构造函数都补两行断言,就为了让统计面板上的数字好看点。真正核心的、要算钱、要判权限、要走规则的函数,反而连个测试影子都没有。

不过话说回来,谁没写过凑数的测试呢?我刚工作那会,为了过部门的覆盖率检查,连个空的构造函数都写了三行测试,现在想起来都觉得好笑。
其实单元测试的本质特别简单——你把自己写的那几十行、上百行的独立函数拆出来,不依赖数据库、不依赖外部接口,就输入不同的参数,看输出是不是符合你当初写这个函数时的预期。你不需要测整个下单流程从用户点按钮到支付成功的全链路,那是集成测试或者端到端测试的活,你要做的就是把最小块的逻辑摸透,别等三个月后自己都记不清当时写的判断是包含边界还是不包含边界,改个参数就把整个链路搞崩。
就这么简单。
真到线上出问题的时候你就知道值了
之前我待过的一个电商团队,和小周的团队情况差不多,大家一开始也吐槽写单元测试太耽误时间——本来写业务逻辑就要两天,写测试还要多花大半天,完全是额外工作量。
直到去年双11前,财务突然说要把跨境订单的税费计算规则改了:之前是统一按订单金额的9%收税,现在要改成阶梯计费,0到500块的部分按9%算,500到1000的部分按7%算,1000以上的部分按5%算。
巧的是,当时负责税费模块的开发已经离职半年了,换别的团队,指不定要对着几千行的旧代码翻个底朝天,边看边猜边界值在哪,改完了还得提心吊胆怕漏了场景。
但那个团队的人,直接打开对应函数的单元测试文件,把所有旧规则下的测试用例找出来,对着新规则改了预期的输出值,再调整函数里的计算逻辑,跑一遍测试全绿就上线了。
整个过程花了不到三个小时。

没出线上事故。
那天晚上大家还正常点了外卖,没人熬夜盯着监控等告警,更不用爬起来回滚版本。
说真的,我见过太多开发总觉得自己脑子好,写过的代码过半年也能记得清清楚楚,真到改逻辑的时候,满脑子都是“我当时为啥要写这个判断?这里的边界值到底包不包含?”,越想越慌,越慌越容易写错。就像之前认识的一个做教育系统的朋友,写课程到期判断的函数时图省事没写测试,后来产品说要把“付费后7天自动解锁课程”改成“付费后7天如果用户没完成第一节练习就不解锁”,他改完代码直接上线,结果把所有已经付费的老用户的课程全锁了——因为他完全忘了考虑老用户是在旧规则下买的课,应该不受新规则约束。
赔了好几十张满减优惠券。
他后来跟我说,那天晚上盯着代码看了三个小时,才发现自己漏了判断用户的付费时间是否在新规则生效之前,要是当时写了哪怕三五个测试用例,把老用户、新用户、卡在时间节点上的用户场景都测一遍,根本不会出这种低级错误。
别把单元测试搞成没人扛的烂摊子
很多团队推单元测试推不下去,根本不是开发抵触,是从一开始的规则就定歪了。
别一上来就拍脑袋要求全团队100%覆盖率,那肯定会催生一堆凑数的垃圾测试,最后没人愿意维护。你完全可以从最小的要求开始:所有涉及金额计算、权限判断、规则校验的核心函数,必须配至少3个测试用例:正常输入的情况,边界值的情况,异常输入的情况。就够了。
测试代码要和业务代码放在同一个仓库里,别搞什么单独的测试仓库,那样大家根本懒得同步维护。谁写的函数,谁配测试,代码评审的时候,别光盯着业务逻辑写得好不好,也要看对应的测试是不是真的测到了核心逻辑,不是那种凑数的无意义断言。
测试要稳。
好的单元测试,是你改了函数内部的实现逻辑,只要输入输出符合最初的约定,测试就不应该报错。要是你的测试天天因为你改了个无关紧要的变量名,或者把字符串的格式调整了个空格就亮红叉,用不了半年,整个团队的测试文件就会变成没人愿意碰的垃圾场——大家每次看到测试红了都懒得管,反正也不知道是真的bug还是测试本身写得太脆弱。
对吧,本来写代码就是为了少给自己找事,别把单元测试搞成新的负担。
上周我再去小周的公司,发现他们后端组已经把核心的优惠券计算逻辑的测试补上了。小周说,现在改满减规则之前,先跑一遍测试,心里踏实多了。
哦对,他上次出问题赔的那几百块优惠券,这个季度的绩效扣完,刚好把之前没写测试省的时间全补回来了。
挺真实的。