我为什么把单元测试从“走过场”变成了“必修课”
我为什么把单元测试从“走过场”变成了“必修课”
说实话,我以前对单元测试的态度是:能跑就行,能不写就不写。
理由很充分:需求天天变,写完就改,测了也白测;业务逻辑简单,肉眼 review 就够了;时间紧,写测试不如多写两个接口。这些话听着耳熟吧?我以前就是这么跟同事、跟自己说的。
直到有一次,我改了一个“简单到不能再简单”的工具方法——把一个字符串截取函数从按字符数截取改成按字节数截取。改动不到 5 行,自测也没问题,就直接上了生产。结果第二天客服炸了:一批用户的昵称显示乱码,有的直接变成空字符串。原因很简单,我没考虑到 emoji 在 UTF-8 里占 4 个字节,按字节截取直接把 emoji 截断了。
如果当时我写了一个单元测试,覆盖一下 emoji、中文、中英文混排、边界长度这些 case,这个 Bug 根本走不出我的本地环境。但现实是,我花了整整一天处理客诉、修数据、发补丁。
那次之后,我强制自己给核心工具类和关键业务路径补单元测试。坚持了半年,发现单元测试给我的回报远不止“少出 Bug”这么简单。
一、测试是代码的“第一用户”
写测试的过程,其实是在从调用者的视角审视自己的代码。如果你发现一个方法很难写测试——要 mock 七八个依赖、要构造一堆复杂参数、要绕开各种静态方法——那大概率不是测试难写,是你的代码设计有问题。
我有一个工具类,最初写了十几个静态方法,互相调用,牵一发动全身。我想给其中某个方法写单元测试,结果发现得把整个类都 mock 进去。这逼着我做了一次重构:拆成两个职责单一的独立类,每个类只依赖自己需要的组件。重构完再写测试,每个方法两三行就搞定了。
测试倒逼出了更好的设计,这是我没想到的副作用。
二、测试是重构的“安全绳”
后来我们有一次大重构,要把一个跑了三年的老模块从单体里拆出去。如果没有单元测试,我根本不敢动——谁知道改完哪里会炸?但因为核心逻辑都有测试覆盖,我每改一步就跑一遍测试,看到绿色对勾就继续,看到红色就立刻修正。最终整个重构用了三天,零生产事故。
对比一下:之前另一个团队重构类似模块,没测试,上线后连出三天问题,最后不得不回滚。
三、测试文档化了“隐藏逻辑”
代码只告诉你怎么做,测试告诉你为什么这么做。比如下面这个测试:
@Test
void should_return_false_when_order_expired_and_already_paid() {
Order order = new Order(PAID, LocalDateTime.now().minusDays(30));
assertFalse(order.canRefund());
}
它不只验证了代码行为,还记录了业务规则:已支付且超过 30 天的订单不能退款。新人接手代码,看测试比看文档快得多。
一个务实的建议:不用追求 100% 覆盖率
很多团队把覆盖率当 KPI,要求 80%、90%,甚至 100%。我觉得没必要。我的策略是抓重点:
- 工具类、算法、状态机、金额计算——这些必须测,出错了后果严重。
- 纯 CRUD 的 Controller——测个参数校验和正常流程就行,不用每个分支都覆盖。
- 外部调用、中间件交互——用集成测试覆盖,单元测试里 mock 掉。
与其追求数字好看,不如让每一行测试都真正保护一段关键逻辑。
现在我团队的规矩是:改老代码,先补测试;写新代码,测试跟代码一起提交。不要求完美,但要求你认真对待。因为单元测试不是写给领导看的,是写给那个未来凌晨三点被叫起来救火的自己看的。
如果你现在还在纠结“要不要写测试”,我的建议是:从明天开始,给下一个你写的工具方法加一个测试用例。就一个。写完你会发现,那种“有人替你把着关”的安全感,真香。
- 点赞
- 收藏
- 关注作者
评论(0)