24、测试代码风格与规范:命名约定、注释要求、测试代码的可维护性

说实话,做了这么多年嵌入式开发,我见过太多「能跑就行」的测试代码了。嗯,能跑确实能跑,但三个月后你自己都看不懂写了什么。我个人习惯是:测试代码的规范程度,直接决定了这个项目能走多远

你想想看,产品代码写得再漂亮,如果测试代码一团糟,谁敢放心改?我就在项目中遇到过,一个同事为了赶进度,测试函数命名全是 test1test2……后来出 bug 了,根本不知道哪个测试在测什么。所以今天咱们好好聊聊测试代码的风格与规范。

核心观点:测试代码不是二等公民。它和产品代码一样,需要命名规范、注释清晰、结构可维护。

一、命名约定:让名字自己说话

好的命名,就是最好的注释。我个人习惯用 三段式命名法

// 格式:test_[被测函数]_[测试场景]_[预期结果]
void test_uart_send_normal_returnsOk(void);
void test_uart_send_bufferFull_returnsError(void);
void test_timer_start_alreadyRunning_returnsFalse(void);

为什么要这么啰嗦?因为当你看到 test_uart_send_bufferFull_returnsError 时,根本不需要看代码就知道它在测什么。我曾经接手过一个项目,测试函数叫 test_uart_1test_uart_2……打开一看,全是几百行的代码,鬼知道每个在测什么。

组件 命名规则 示例
测试函数 test_[模块]_[场景]_[预期] test_flash_write_full_returnsError
测试文件 [模块]_test.c / [模块]_test.h uart_test.c
测试套件 suite_[模块] suite_uart
辅助函数 helper_[功能] helper_create_temp_buffer

小技巧:测试函数名可以长一点,但一定要清晰。我一般控制在 40 个字符以内,太长了就说明测试场景太复杂,该拆分了。

二、注释要求:写给人看的,不是写给机器看的

测试代码的注释,重点不是「做了什么」,而是「为什么这么做」。说白了,代码本身已经告诉你怎么做了,注释要解释的是背后的逻辑。

我建议遵循 三层次注释法

  1. 文件头注释:说明这个测试文件测什么模块、依赖什么环境
  2. 函数级注释:说明测试场景、前置条件、预期结果
  3. 关键步骤注释:说明为什么这么设置参数、为什么这个边界值有意义
/**
 * @brief 测试 UART 在接收缓冲区满时的行为
 *
 * 前置条件:
 * - UART 已初始化,波特率 115200
 * - 接收缓冲区大小为 256 字节
 *
 * 测试步骤:
 * 1. 连续发送 257 字节数据
 * 2. 检查是否返回 BUFFER_FULL 错误
 * 3. 检查缓冲区内容是否被覆盖
 *
 * 预期结果:
 * - 返回错误码 ERR_BUFFER_FULL
 * - 缓冲区前 256 字节数据完整
 */
void test_uart_receive_bufferFull_returnsError(void)
{
    // 初始化 UART,使用默认配置
    uart_init(UART_0, 115200);

    // 填充缓冲区到满 —— 这里用 257 是因为缓冲区是 256
    uint8_t data[257];
    memset(data, 0xAA, sizeof(data));

    // 发送数据,预期第 257 字节会触发错误
    int result = uart_send(UART_0, data, sizeof(data));

    // 验证
    TEST_ASSERT_EQUAL(ERR_BUFFER_FULL, result);
}

注意:不要写废话注释。比如 // 初始化变量 这种,删掉吧,没意义。我见过最离谱的注释是 i++ // i 加 1……嗯,这种注释还不如不写。

三、测试代码的可维护性:别让测试成为负担

测试代码也是代码,它也会腐烂。我见过太多项目,测试代码一开始写得挺好,半年后没人敢碰——因为一改就全崩。为什么会这样?说白了,就是可维护性没做好。

3.1 避免硬编码

把魔数、配置参数提取成宏或常量。这样改一个地方,所有测试都跟着变。

// ❌ 坏例子
void test_buffer_write(void)
{
    uint8_t buf[256];
    // 为什么是 256?为什么是 0x55?
    for (int i = 0; i < 256; i++) {
        buf[i] = 0x55;
    }
}

// ✅ 好例子
#define TEST_BUFFER_SIZE    256
#define TEST_FILL_PATTERN   0x55

void test_buffer_write(void)
{
    uint8_t buf[TEST_BUFFER_SIZE];
    for (int i = 0; i < TEST_BUFFER_SIZE; i++) {
        buf[i] = TEST_FILL_PATTERN;
    }
}

3.2 测试之间要独立

每个测试函数应该是独立的。不要依赖其他测试的执行顺序。我曾经踩过一个坑:测试 A 初始化了某个全局变量,测试 B 依赖这个状态。后来有人改了测试顺序,B 就挂了。排查了半天……

黄金法则:每个测试函数都能单独运行,且结果不依赖其他测试。

3.3 使用 setUp 和 tearDown

把公共的初始化和清理工作提取出来。这样每个测试函数只关注自己的核心逻辑。

static UART_HandleTypeDef huart;

void setUp(void)
{
    // 每个测试前都重新初始化
    uart_init(&huart, 115200);
    uart_enable(&huart);
}

void tearDown(void)
{
    // 每个测试后都清理
    uart_disable(&huart);
    uart_deinit(&huart);
}

void test_uart_send_normal(void)
{
    uint8_t data[] = "Hello";
    int result = uart_send(&huart, data, sizeof(data));
    TEST_ASSERT_EQUAL(OK, result);
}

void test_uart_send_empty(void)
{
    int result = uart_send(&huart, NULL, 0);
    TEST_ASSERT_EQUAL(ERR_INVALID_PARAM, result);
}

3.4 测试代码也要重构

如果你发现测试代码里有重复的片段,别犹豫,抽成辅助函数。我见过一个测试文件,同样的初始化代码写了 20 遍……后来改了个参数名,改了 20 个地方,还漏了两个。

// 辅助函数:创建一个预填充的缓冲区
static void helper_fill_buffer(uint8_t *buf, size_t size, uint8_t pattern)
{
    for (size_t i = 0; i < size; i++) {
        buf[i] = pattern;
    }
}

// 测试函数里直接调用
void test_buffer_checksum(void)
{
    uint8_t buf[64];
    helper_fill_buffer(buf, sizeof(buf), 0xAA);

    uint32_t checksum = calc_checksum(buf, sizeof(buf));
    TEST_ASSERT_EQUAL(0xAA * 64, checksum);
}

四、知识体系总览

下面这张图,是我对测试代码风格与规范的理解。你可以把它当作一个检查清单:

测试代码风格与规范 · 知识体系 命名约定 注释要求 可维护性 三段式命名法 文件/函数/套件命名规则 辅助函数命名规范 文件头注释 函数级注释(场景/前置/预期) 关键步骤注释(为什么) 避免硬编码 测试独立(无依赖) setUp / tearDown 模式 及时重构,消除重复 目标:测试代码像产品代码一样专业

五、避坑指南

最后,分享几个我踩过的坑:

  • 我曾经在测试代码里直接用了产品代码的全局变量,结果产品代码重构后,测试全崩了。后来我改用接口隔离,测试只依赖 API,不依赖内部实现。
  • 我曾经写了一个 500 行的测试函数,里面嵌套了 3 层 if-else。后来我自己都看不懂了。记住:一个测试函数只测一个场景。
  • 我曾经为了省事,把测试代码和产品代码混在一起编译。结果产品发布时忘了去掉测试代码……嗯,那次被领导骂得不轻。现在我一定用条件编译隔离。

我的建议:把测试代码当作产品代码来写。命名规范、注释清晰、结构合理。这样你才能在项目后期放心地修改代码,而不用担心「改了一行,崩了一片」。

好了,关于测试代码的风格与规范,今天就聊到这里。记住:好的测试代码,是项目长期健康的保障。别偷懒,别将就。