第三章 测试驱动开发(TDD):红绿重构循环

说实话,我第一次接触TDD时,心里是抵触的。

那时候我在做一个内窥镜的图像处理模块,代码量已经很大了。让我先写测试再写代码?这不是浪费时间吗?

直到有一次,我改了一行像素校正的算法,结果把白平衡给搞崩了。调试了两天才发现,是那个看似无关的改动引发的连锁反应。从那以后,我老老实实开始用TDD。

3.1 红绿重构循环到底是什么?

TDD的核心就三个步骤,我管它叫「红绿重构」:

  1. :先写一个会失败的测试
  2. 绿:写最简代码让测试通过
  3. 重构:优化代码,保证测试依然通过

你想想看,这其实是个很聪明的做法。先定义「什么是正确的」,再去实现它。而不是先写一堆代码,再回头想怎么测。

核心原则:没有失败的测试,就不写生产代码。

3.2 一个内窥镜的TDD实战例子

咱们拿内窥镜的「图像亮度检测」功能来说。需求很简单:如果画面太暗,就报警。

第一步:写一个会失败的测试(红)

// test_brightness_detector.c
#include <unity.h>

void test_detect_dark_image_should_return_true(void) {
    // 创建一个全黑的图像
    uint8_t dark_image[100] = {0};
    
    // 调用检测函数
    bool result = is_image_too_dark(dark_image, 100);
    
    // 断言:应该返回true
    TEST_ASSERT_TRUE(result);
}

void test_detect_bright_image_should_return_false(void) {
    // 创建一个全白的图像
    uint8_t bright_image[100];
    memset(bright_image, 255, 100);
    
    bool result = is_image_too_dark(bright_image, 100);
    
    TEST_ASSERT_FALSE(result);
}

这时候编译,肯定报错——因为 is_image_too_dark 这个函数还没写呢。嗯,这就是「红」的状态。

第二步:写最简代码让测试通过(绿)

// brightness_detector.c
bool is_image_too_dark(const uint8_t* image, uint32_t size) {
    uint32_t sum = 0;
    for (uint32_t i = 0; i < size; i++) {
        sum += image[i];
    }
    uint8_t avg = sum / size;
    return avg < 50;  // 平均亮度低于50就认为太暗
}

跑一下测试,两个都通过了。绿了!

第三步:重构

代码虽然能跑,但我觉得阈值50写死了不太好。内窥镜在不同场景下,对亮度的要求不一样。比如检查胃部时,环境光本来就暗。

// 重构后的代码
typedef struct {
    uint8_t threshold;  // 可配置的亮度阈值
} BrightnessConfig;

bool is_image_too_dark(const uint8_t* image, uint32_t size, BrightnessConfig* config) {
    uint32_t sum = 0;
    for (uint32_t i = 0; i < size; i++) {
        sum += image[i];
    }
    uint8_t avg = sum / size;
    return avg < config->threshold;
}

重构完,再跑一遍测试。如果测试全过,说明改动没破坏原有逻辑。

我的习惯:每次重构后,我都会把测试跑三遍。第一遍确认通过,第二遍故意改坏代码看测试会不会失败,第三遍改回来再确认。这样能确保测试本身没问题。

3.3 TDD在内窥镜开发中的实际应用

我在做内窥镜项目时,发现TDD特别适合这几个场景:

应用场景 为什么适合TDD 我踩过的坑
图像处理算法 输入输出明确,容易定义预期结果 曾经没考虑边界情况,比如图像尺寸为0时程序崩溃
通信协议解析 协议帧格式固定,可以构造各种测试用例 漏测了异常帧,导致设备偶尔死机
传感器驱动 可以模拟传感器返回值,覆盖各种工况 模拟数据太理想化,真实传感器有噪声
状态机控制 每个状态转换都可以写一个测试 状态太多,测试用例写到手软

3.4 避坑指南

我曾经犯过的三个错误:

  • 测试写得太复杂,一个测试用例测了七八个功能。结果失败了都不知道是哪一步出的问题。
  • 重构时改了接口,忘了更新测试。测试一直绿,但代码已经跑不通了。
  • 为了追求100%覆盖率,写了一些「假测试」——只测了getter/setter,核心逻辑反而没测。

我的建议是:

  • 每个测试只测一件事。名字要起清楚,比如 test_dark_image_should_trigger_alarm
  • 接口变了,第一时间改测试。别拖,拖了就忘了。
  • 覆盖率是手段,不是目的。重点测那些容易出错的逻辑分支。

3.5 红绿重构的节奏感

做TDD其实有个节奏。我个人习惯是:

  1. 先想清楚这个功能要做什么,写一个测试
  2. 跑测试,确认是红的(失败)
  3. 写最笨的代码让它变绿
  4. 停下来,看看能不能优化
  5. 重复

这个循环越快越好。我一般控制在5分钟以内。如果超过10分钟还没变绿,说明测试写得太大了,拆小一点。

记住:TDD不是为了测试而测试。它是一种设计方法。通过写测试,你其实是在思考「这个接口该怎么设计才方便调用」。我很多好的接口设计,都是在写测试时想出来的。

好了,红绿重构循环就讲到这里。下一章咱们聊聊覆盖率分析,看看怎么用数据说话,找出代码里的「盲区」。