单元测试是浪费时间?用对的人把它当重构安全网
“写单元测试是浪费时间”“有那功夫功能都写完了”——这种争论几乎在每个技术团队都出现过。另一边的故事是:某次重构把线上改崩了,大家才想起那个“早就该补却没补”的测试。真实的图景介于两者之间:单元测试本身不保证质量,用对了是安全网,用错了是负担。差别在于,你是否理解它真正解决什么问题。
先看一个大背景:软件迭代越来越快,质量保障不能靠手工。手工测试依赖人,且随版本迭代迅速退化;而自动化测试可以随代码提交反复运行。Google内部自动化测试的分层投入比例是约70%单元测试、20%接口测试、10%UI测试——金字塔形状本身,就是答案。
单元测试的真正价值:反馈速度与安全网
单元测试是函数或类级别、不依赖外部系统(可用Mock隔离)、毫秒级运行的测试。它的价值不在“证明当前代码正确”,而在三件事:一是回归保护——重构时有人替你把关,改坏立刻报警;二是快速反馈——在合并之前、成本最低的时候发现问题,而缺陷发现得越晚,修复成本越高;三是倒逼设计——一段代码如果怎么写都测不了,往往说明它耦合太重、职责不清,测试在替你指出设计问题。
测试金字塔是组织自动化测试的基本原则:底层是大量的单元测试(快、便宜、易维护),中间是适量的接口与集成测试,顶层是少量的端到端测试。常见的反面案例是“头重脚轻”:团队迷信端到端测试,堆了大量模拟用户操作的UI测试——它们跑得慢、改起来难,很快拖垮持续集成,高频上线从此遥不可及。
什么值得测:测行为,不测实现
软件工程大师马丁·福勒在《实用测试金字塔》里提醒过一个关键边界:单元测试要测可观察的行为,而不是内部实现结构。正确的问法是“如果我输入x和y,输出是z吗”,而不是“这个方法应该先调用A类、再调用B类”。私有方法通常属于实现细节,你甚至不该有测试它的冲动——一旦测试和实现绑得太紧,每次重构测试就碎一片,安全网就变成了绊脚石。
具体到测什么:正常路径要测,边界条件更要测——空值、零值、超长输入、并发场景,bug大多藏在边界里。每个测试只验证一个条件,用“准备—执行—断言”的结构组织,让测试能像文档一样被人读懂。还有一条容易被忽视的原则:测试代码与生产代码同等重要,“只是测试代码而已”不是写得粗糙的借口——测试也是要长期维护的资产。
一个案例:从“不敢重构”到“放心重构”
一个支付模块的故事很有代表性:历史代码没人敢动,每次加需求都“绕着走”——因为没人说得清改动会影响哪条路径。新来的工程师花了两个星期,先为关键路径补了四十多个单元测试:金额计算、折扣叠加、余额不足、并发扣款这些边界场景全部覆盖。补完后他开始重构内部结构,每改一步跑一次全量测试,绿灯就继续,红灯就回退。重构完成时测试全绿,上线零故障。团队从此立下约定:核心逻辑的改动必须带测试。有意思的是,此后需求开发速度反而变快了——以前改代码要反复人工验证,现在跑一遍测试就知道有没有改坏。
常见误区:让测试变成负担的五种写法
- 追求100%覆盖率:覆盖率是参考指标,不是质量指标。为了覆盖而给getter、setter写测试,只会让维护负担翻倍,还培养出“为指标而写”的形式主义。
- 测实现细节:重构一次碎一片,测试从帮手变成“改代码的敌人”,团队会本能地开始绕过测试。
- 只测正常路径:happy path全绿,边界和异常全裸奔——而线上故障几乎都发生在边界。
- 让单测依赖数据库和外部服务:跑得慢、不稳定,今天绿明天红,慢慢就没人信了。外部交互应该交给集成测试,单测里用Mock隔离。
- 写完不维护:测试和代码一起腐烂,最终全量飘红,团队选择“忽略测试”——这比没有测试更危险,因为它给了你虚假的安全感。
行动建议
- 从核心模块开始:挑最近改动最频繁、出过线上bug的模块,先补正常路径和边界条件的测试。
- 遵守“一测一断言”和“准备—执行—断言”结构,让测试能当文档读。
- 重构前先跑全量测试,把红灯当成“保险丝断了”的信号,而不是绕过去的障碍。
- 把覆盖率当参考不当KPI,和团队约定:核心业务逻辑的改动必须伴随测试,评审时一起看。
单元测试的本质,是用一次性的编码成本,购买未来每一次改动的安全感。写测试不是给代码上坟,而是给代码上保险——而保险,总是在出事之前买最便宜。