24、TDD与代码审查:测试如何辅助代码审查?审查测试用例的要点
代码审查这件事,很多团队都在做。但说实话,我见过不少审查流于形式——走马观花看一遍,改几个命名就过了。真正有价值的审查,应该像法庭上的交叉质证,而测试用例就是你的关键证人。
今天我们就聊聊,TDD 写出来的测试,怎么帮我们把代码审查做得更扎实。反过来,审查测试用例本身,又该盯住哪些要点。
测试是代码审查的「活文档」
你想想看,一段代码到底想干什么?注释可能过时,需求文档可能没人更新。但测试用例不会撒谎——它实实在在地描述了代码的行为。
我个人习惯,审查代码时先看测试。为什么?因为测试告诉我三件事:
- 这个函数/类应该做什么(从测试命名和断言中读出来)
- 边界情况有没有被覆盖(空值、异常、极限值)
- 代码的可测试性如何(如果测试写得很别扭,说明设计有问题)
我在项目中遇到过好几次,开发提交的代码逻辑看起来没问题,但一翻测试,发现全是「快乐路径」——只测了正常情况,异常分支一个都没覆盖。这种审查,测试就是照妖镜。
审查测试用例的六大要点
好,那具体到审查测试用例本身,该看什么?我总结了六个要点,每个都是踩过坑才明白的。
1. 测试命名是否清晰表达了意图
我曾经看到过一个测试叫 test1(),打开一看,测了一堆东西。这种测试基本等于没写。好的测试命名应该像一句陈述句:
// 不推荐
TEST_F(ShoppingCartTest, test1) { ... }
// 推荐
TEST_F(ShoppingCartTest,
should_throw_exception_when_adding_negative_quantity) { ... }
审查时我会问:不看代码,光看测试名,能知道它在测什么吗?如果答案是否定的,那就得改。
2. 是否遵循了「单一断言」原则
这里说的「单一断言」,不是指一个测试里只能写一个 assert,而是指一个测试只验证一个行为。我见过一个测试里同时验证了价格计算、库存扣减、订单状态更新——结果失败了,你根本不知道是哪个环节出了问题。
// 不推荐:一个测试测了太多东西
TEST_F(OrderTest, should_process_order_correctly) {
auto order = createOrder(10, 100.0);
processOrder(order);
ASSERT_EQ(order.totalPrice(), 1000.0);
ASSERT_EQ(order.stock(), 90);
ASSERT_EQ(order.status(), OrderStatus::PAID);
}
// 推荐:拆成三个独立的测试
TEST_F(OrderTest, should_calculate_total_price_correctly) { ... }
TEST_F(OrderTest, should_deduct_stock_after_processing) { ... }
TEST_F(OrderTest, should_set_status_to_paid_after_processing) { ... }
审查时,如果一个测试里断言超过三个,我就会警惕——是不是该拆了?
3. 测试数据是否「自解释」
我最怕看到这样的测试:
TEST_F(CalculatorTest, should_add_two_numbers) {
ASSERT_EQ(add(3, 5), 8);
}
3 和 5 是什么?8 又是什么?虽然能看懂,但不够好。更好的做法是用有意义的变量名:
TEST_F(CalculatorTest, should_add_two_positive_numbers) {
int firstNumber = 3;
int secondNumber = 5;
int expectedSum = 8;
ASSERT_EQ(add(firstNumber, secondNumber), expectedSum);
}
审查时,如果测试数据是「魔法数字」,我会建议改成具名常量。这样别人读测试时,不用在脑子里做算术。
4. 边界条件和异常路径是否覆盖
这是审查的重灾区。很多开发只写了「正常情况」的测试,边界条件全漏了。我一般会对照代码中的 if、switch、循环边界,逐一检查测试是否覆盖:
- 空值/空集合
- 最大值/最小值
- 负数/零
- 异常输入(如非法格式)
- 并发场景(如果适用)
5. 测试是否独立且可重复
测试之间不能有依赖。我见过一个测试依赖另一个测试执行后留下的数据库记录——结果跑单个测试时全挂了。审查时我会检查:
- 每个测试是否自己准备数据、自己清理数据
- 测试之间是否共享了可变状态
- 测试能否以任意顺序执行
一个简单的验证方法:把测试顺序打乱跑一遍,如果结果不一致,说明有依赖。
6. 测试速度是否合理
单元测试应该快。如果审查时发现一个测试里调了数据库、发了 HTTP 请求、写了文件,那它就不是单元测试,而是集成测试。我会建议:
- 把外部依赖用 Mock 替换
- 把慢的测试移到单独的测试套件中
- 确保核心业务逻辑的测试能在毫秒级完成
测试如何辅助代码审查?
说完了审查测试本身,我们再反过来看:测试怎么帮我们审查生产代码?
我总结了一个「测试驱动审查」的流程,用一张图来说明:
这个流程的核心是:测试先行,代码后审。先通过测试建立对代码行为的预期,再对照实现看是否匹配。如果测试覆盖了边界情况但代码没处理,那就是 bug;如果代码有逻辑但测试没覆盖,那就是遗漏。
审查中的常见「测试坏味道」
最后,我列几个审查时一眼就能发现的坏味道,遇到了直接打回:
| 坏味道 | 表现 | 建议 |
|---|---|---|
| 测试太「聪明」 | 测试里用了循环、条件判断、复杂计算 | 测试应该是简单的线性代码,复杂逻辑本身就需要测试 |
| Mock 过多 | 一个测试里 Mock 了 5 个以上对象 | 说明被测对象耦合太深,考虑重构 |
| 测试与实现耦合 | 测试里调用了私有方法或依赖了内部实现细节 | 测试应该只测公开接口,不关心内部实现 |
| 测试有副作用 | 测试执行后改变了全局状态或文件系统 | 每个测试应该独立且可重复,清理所有副作用 |
| 测试命名模糊 | testSomething、checkXXX、verifyYYY | 用 should_xxx_when_yyy 的格式,一目了然 |
嗯,说到底,测试和代码审查是相辅相成的。好的测试让审查变得高效,而审查又能反过来提升测试质量。我在团队里推行过一个规则:没有测试的代码,不进入审查流程。刚开始大家觉得麻烦,但坚持了两个月后,线上 bug 减少了 60% 以上。
所以,下次审查代码时,别忘了先看看测试。它可能是你发现问题的第一道防线。