单元测试到底在测什么
一、争议在于大家测的东西不一样
有人写测试是为了验证逻辑正确,有人是为了重构时不回归,有人是为了写文档。目的不同,测法的差别很大,吵起来谁也没错。
我自己的判断是:单元测试最值钱的产出不是"它跑了多少个用例",而是"以后改这块代码我敢不敢动"。
二、真正该测的和经常被测错的
该测的:分支和边界。
// 值得测:三种输入走向三条不同路径
function level(score) {
if (score >= 90) return 'A';
if (score >= 60) return 'B';
return 'C';
}
level(95); // 'A'
level(60); // 'B' ← 边界值,最容易漏
level(59); // 'C'
不值得单独测的:getter/setter 和拼装代码。
// 这种测试没有任何价值,get 天然返回 set 进去的值
expect(user.getName()).toBe('张三');
它测的是语言本身,不测业务。写它只是为了让覆盖率数字好看。
三、测代码而不是测实现
这是最关键的一条,也是重构最常踩的坑。
// 测实现细节:改内部结构就全红,业务其实没变
expect(calc.compute()).toBe(30);
// 测行为:只要对外表现不变,改内部怎么改都安全
expect(calc.add(10, 20)).toBe(30);
前一种测法下,代码里换一种方式求和,测试就挂了,但功能完全正常。团队会开始觉得测试是负担,然后有人去改测试迁就代码,最后测试就彻底失守了。
判断标准很简单:你的测试在代码重构时会红吗? 会,说明测到实现细节了。
四、覆盖率这个数字的正确用法
它是找盲区的工具,不是质量指标。
- 某个文件覆盖率 0%:这块完全没被测过,看看是不是重要逻辑。
- 某个分支覆盖率低:有一半情况没被覆盖,通常就是边界条件。
- 某行覆盖了但断言很弱:比如只断言"不抛异常",等于没测。
反过来,追求整体覆盖率到 90% 往往会写出一堆凑数的测试。数字达标了,重构时照样不敢动。
五、一个务实的做法
一、先给核心的纯函数写测试。 输入输出明确、没有副作用的函数,测起来最省力,收益最高。
二、涉及数据库和网络的不写单元测试,写集成测试。 硬把数据库 mock 掉测出来的通过率没有意义,因为你 mock 的那层正是最常出问题的地方。
三、测试名写清楚意图。 测试折扣计算 不如 满200减30时返回170。失败时从测试名就能看出哪个行为坏了。
四、让失败信息可读。 断言里带上实际值,排查时不用重跑一遍。
六、最后说句实话
单元测试有成本,尤其是逻辑简单的项目——花三倍时间测一个只有三行的函数,性价比不高。这种情况不如把精力放在手工验证上。
但一旦代码涉及金额、权限、状态流转、多端一致性,测试的性价比会陡增,因为这些东西错一次的代价远大于写测试的时间。这是我实际的判断线:按后果的严重程度决定要不要测,不按覆盖率决定。
- 点赞
- 收藏
- 关注作者
评论(0)