4. 测试固件与测试用例分离

任务说明

本案例针对嵌入式C语言单元测试中常见的资源管理问题:多个测试用例需要重复执行相同的初始化与清理操作,若直接复制粘贴,会导致代码冗余且难以维护。目标是将初始化与清理逻辑封装为测试固件(setUp/tearDown),使测试用例仅关注自身断言逻辑,同时保证每个用例执行前后资源状态一致。

测试固件设计

测试固件采用setUp/tearDown模式:setUp()在每个测试用例执行前被调用,负责分配资源并设置初始状态;tearDown()在每个测试用例执行后调用,负责释放资源并恢复环境。测试框架通过注册表管理用例,执行时自动按“setUp → 用例函数 → tearDown”的顺序调用。本案例使用轻量级自研框架,不依赖外部库,便于在嵌入式环境中直接编译运行。

被测模块:环形缓冲区

被测对象为固定容量环形缓冲区,提供初始化、写入、读取三个接口。缓冲区内部使用静态数组存储数据,通过头尾指针管理读写位置。该模块在嵌入式场景中常用于串口数据缓存,其资源管理(容量限制、指针回绕)适合验证固件复用效果。

// ringbuf.h
#ifndef RINGBUF_H
#define RINGBUF_H

#include <stdint.h>
#include <stddef.h>

#define RBUF_CAPACITY 4

typedef struct {
    uint8_t data[RBUF_CAPACITY];
    size_t head;
    size_t tail;
    size_t count;
} rbuf_t;

void rbuf_init(rbuf_t *rb);
int rbuf_write(rbuf_t *rb, uint8_t byte);
int rbuf_read(rbuf_t *rb, uint8_t *byte);

#endif
// ringbuf.c
#include "ringbuf.h"

void rbuf_init(rbuf_t *rb) {
    rb->head = 0;
    rb->tail = 0;
    rb->count = 0;
}

int rbuf_write(rbuf_t *rb, uint8_t byte) {
    if (rb->count >= RBUF_CAPACITY) {
        return -1;  // 缓冲区满
    }
    rb->data[rb->head] = byte;
    rb->head = (rb->head + 1) % RBUF_CAPACITY;
    rb->count++;
    return 0;
}

int rbuf_read(rbuf_t *rb, uint8_t *byte) {
    if (rb->count == 0) {
        return -1;  // 缓冲区空
    }
    *byte = rb->data[rb->tail];
    rb->tail = (rb->tail + 1) % RBUF_CAPACITY;
    rb->count--;
    return 0;
}

测试框架与固件实现

测试框架维护一个用例函数指针数组,RUN_TEST宏负责按固定顺序调用setUp、用例、tearDown。固件中setUp()初始化缓冲区并写入两个预置字节,tearDown()重置缓冲区状态,确保用例间无残留数据。

// test_framework.h
#ifndef TEST_FRAMEWORK_H
#define TEST_FRAMEWORK_H

#define MAX_TEST_CASES 8

typedef void (*test_func_t)(void);

void test_runner_run_all(void);
void test_runner_register(test_func_t func, const char *name);

#endif
// test_framework.c
#include "test_framework.h"
#include <stdio.h>

static test_func_t test_cases[MAX_TEST_CASES];
static const char *test_names[MAX_TEST_CASES];
static int test_count = 0;
static int pass_count = 0;
static int fail_count = 0;

void test_runner_register(test_func_t func, const char *name) {
    if (test_count < MAX_TEST_CASES) {
        test_cases[test_count] = func;
        test_names[test_count] = name;
        test_count++;
    }
}

void test_runner_run_all(void) {
    for (int i = 0; i < test_count; i++) {
        printf("[RUN ] %s\n", test_names[i]);
        setUp();
        test_cases[i]();
        tearDown();
    }
    printf("Passed: %d, Failed: %d\n", pass_count, fail_count);
}

void test_assert(int condition, const char *msg) {
    if (condition) {
        pass_count++;
        printf("[PASS] %s\n", msg);
    } else {
        fail_count++;
        printf("[FAIL] %s\n", msg);
    }
}
// test_ringbuf.c
#include "test_framework.h"
#include "ringbuf.h"
#include <stdio.h>

static rbuf_t buffer;

// 测试固件:每个用例前执行
void setUp(void) {
    rbuf_init(&buffer);
    // 预置两个字节,便于测试读取逻辑
    rbuf_write(&buffer, 0x11);
    rbuf_write(&buffer, 0x22);
}

// 测试固件:每个用例后执行
void tearDown(void) {
    // 重置缓冲区,确保用例间隔离
    rbuf_init(&buffer);
}

// 测试用例1:验证初始状态可读取预置数据
static void test_read_preset_data(void) {
    uint8_t byte;
    int ret = rbuf_read(&buffer, &byte);
    test_assert(ret == 0, "read preset data returns 0");
    test_assert(byte == 0x11, "first byte is 0x11");
    ret = rbuf_read(&buffer, &byte);
    test_assert(byte == 0x22, "second byte is 0x22");
}

// 测试用例2:验证写入后数据可完整读出
static void test_write_then_read(void) {
    uint8_t byte;
    // 缓冲区已有2字节,再写2字节填满
    test_assert(rbuf_write(&buffer, 0x33) == 0, "write 0x33 succeeds");
    test_assert(rbuf_write(&buffer, 0x44) == 0, "write 0x44 succeeds");
    // 写入满后应失败
    test_assert(rbuf_write(&buffer, 0x55) == -1, "write when full fails");

    // 依次读出全部4字节
    for (int i = 0; i < 4; i++) {
        test_assert(rbuf_read(&buffer, &byte) == 0, "read succeeds");
    }
    // 读空后应失败
    test_assert(rbuf_read(&buffer, &byte) == -1, "read when empty fails");
}

// 测试用例3:验证缓冲区满时写入失败
static void test_write_when_full(void) {
    uint8_t byte;
    // 当前已有2字节,写入2字节使其满
    rbuf_write(&buffer, 0xAA);
    rbuf_write(&buffer, 0xBB);
    test_assert(rbuf_write(&buffer, 0xCC) == -1, "write returns -1 when full");
    // 缓冲区仍可正常读取
    test_assert(rbuf_read(&buffer, &byte) == 0, "read still works when full");
}

int main(void) {
    test_runner_register(test_read_preset_data, "test_read_preset_data");
    test_runner_register(test_write_then_read, "test_write_then_read");
    test_runner_register(test_write_when_full, "test_write_when_full");
    test_runner_run_all();
    return 0;
}

编译与运行

将上述四个文件(ringbuf.h、ringbuf.c、test_framework.h、test_framework.c、test_ringbuf.c)置于同一目录,使用GCC编译:

gcc -o test_ringbuf ringbuf.c test_framework.c test_ringbuf.c
./test_ringbuf

验证方法与判定标准

运行程序后,观察输出。判定通过的准则为:

  • 每个测试用例名称前显示[RUN],且setUp/tearDown在用例前后自动执行(可通过在固件中添加打印验证,本案例未打印以保持简洁);
  • 所有断言显示[PASS],最终输出Passed: 9, Failed: 0;
  • 若某个用例失败,其后的用例仍能正常执行,证明tearDown正确清理了资源,未产生用例间污染。

执行时序与固件调用关系

测试固件执行时序 test_runner_run_all setUp() 测试用例函数 tearDown 循环 i=0 rbuf_init + 写入0x11,0x22 test_read_preset_data rbuf_init 循环 i=1 rbuf_init + 写入0x11,0x22 test_write_then_read 框架调度 setUp 测试用例 tearDown

固件复用效果说明