第三章 测试驱动开发(TDD):红绿重构循环
说实话,我第一次接触TDD时,心里是抵触的。
那时候我在做一个内窥镜的图像处理模块,代码量已经很大了。让我先写测试再写代码?这不是浪费时间吗?
直到有一次,我改了一行像素校正的算法,结果把白平衡给搞崩了。调试了两天才发现,是那个看似无关的改动引发的连锁反应。从那以后,我老老实实开始用TDD。
3.1 红绿重构循环到底是什么?
TDD的核心就三个步骤,我管它叫「红绿重构」:
- 红:先写一个会失败的测试
- 绿:写最简代码让测试通过
- 重构:优化代码,保证测试依然通过
你想想看,这其实是个很聪明的做法。先定义「什么是正确的」,再去实现它。而不是先写一堆代码,再回头想怎么测。
核心原则:没有失败的测试,就不写生产代码。
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其实有个节奏。我个人习惯是:
- 先想清楚这个功能要做什么,写一个测试
- 跑测试,确认是红的(失败)
- 写最笨的代码让它变绿
- 停下来,看看能不能优化
- 重复
这个循环越快越好。我一般控制在5分钟以内。如果超过10分钟还没变绿,说明测试写得太大了,拆小一点。
记住:TDD不是为了测试而测试。它是一种设计方法。通过写测试,你其实是在思考「这个接口该怎么设计才方便调用」。我很多好的接口设计,都是在写测试时想出来的。
好了,红绿重构循环就讲到这里。下一章咱们聊聊覆盖率分析,看看怎么用数据说话,找出代码里的「盲区」。