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. 边界条件和异常路径是否覆盖

这是审查的重灾区。很多开发只写了「正常情况」的测试,边界条件全漏了。我一般会对照代码中的 ifswitch、循环边界,逐一检查测试是否覆盖:

  • 空值/空集合
  • 最大值/最小值
  • 负数/零
  • 异常输入(如非法格式)
  • 并发场景(如果适用)
避坑指南: 我曾经审查过一个支付模块,所有测试都通过了,但上线后用户输入了超大金额(接近 double 上限),结果溢出了。从那以后,我审查测试时一定会问:「边界值在哪?测了吗?」

5. 测试是否独立且可重复

测试之间不能有依赖。我见过一个测试依赖另一个测试执行后留下的数据库记录——结果跑单个测试时全挂了。审查时我会检查:

  • 每个测试是否自己准备数据、自己清理数据
  • 测试之间是否共享了可变状态
  • 测试能否以任意顺序执行

一个简单的验证方法:把测试顺序打乱跑一遍,如果结果不一致,说明有依赖。

6. 测试速度是否合理

单元测试应该快。如果审查时发现一个测试里调了数据库、发了 HTTP 请求、写了文件,那它就不是单元测试,而是集成测试。我会建议:

  • 把外部依赖用 Mock 替换
  • 把慢的测试移到单独的测试套件中
  • 确保核心业务逻辑的测试能在毫秒级完成

测试如何辅助代码审查?

说完了审查测试本身,我们再反过来看:测试怎么帮我们审查生产代码?

我总结了一个「测试驱动审查」的流程,用一张图来说明:

测试驱动审查流程 1. 先读测试 理解代码行为预期 2. 对照代码 检查实现是否匹配 3. 检查覆盖 边界/异常是否覆盖 ❌ 不通过:打回修改 补充测试或修复代码 ✅ 通过 迭代审查下一段代码 核心原则:测试是审查的起点,不是终点。用测试验证行为,用审查保证质量。 每次迭代:读测试 → 对照代码 → 检查覆盖 → 通过/打回

这个流程的核心是:测试先行,代码后审。先通过测试建立对代码行为的预期,再对照实现看是否匹配。如果测试覆盖了边界情况但代码没处理,那就是 bug;如果代码有逻辑但测试没覆盖,那就是遗漏。

审查中的常见「测试坏味道」

最后,我列几个审查时一眼就能发现的坏味道,遇到了直接打回:

坏味道 表现 建议
测试太「聪明」 测试里用了循环、条件判断、复杂计算 测试应该是简单的线性代码,复杂逻辑本身就需要测试
Mock 过多 一个测试里 Mock 了 5 个以上对象 说明被测对象耦合太深,考虑重构
测试与实现耦合 测试里调用了私有方法或依赖了内部实现细节 测试应该只测公开接口,不关心内部实现
测试有副作用 测试执行后改变了全局状态或文件系统 每个测试应该独立且可重复,清理所有副作用
测试命名模糊 testSomething、checkXXX、verifyYYY 用 should_xxx_when_yyy 的格式,一目了然
一个小技巧: 审查时,我会把测试文件和生产代码文件并排打开。左边是测试,右边是代码。然后问自己:「如果我只读左边,能不能完全理解右边在做什么?」如果答案是否定的,说明测试写得不够好。

嗯,说到底,测试和代码审查是相辅相成的。好的测试让审查变得高效,而审查又能反过来提升测试质量。我在团队里推行过一个规则:没有测试的代码,不进入审查流程。刚开始大家觉得麻烦,但坚持了两个月后,线上 bug 减少了 60% 以上。

所以,下次审查代码时,别忘了先看看测试。它可能是你发现问题的第一道防线。


公众号:蓝海资料掘金营,微信 deep3321