在嵌入式系统中开展C++单元测试,需要兼顾资源限制和硬件依赖特性,选择合适的测试方案才能高效验证代码逻辑的正确性。

嵌入式C++单元测试的核心挑战
嵌入式系统通常存在内存空间小、CPU性能有限、缺少标准输入输出设备等特点,同时很多业务逻辑会和硬件寄存器、外设驱动直接绑定,这给单元测试带来了几个典型问题:
- 无法直接使用PC端常见的测试框架,很多框架依赖标准库和动态内存分配
- 硬件相关代码难以在测试环境中直接运行,需要隔离硬件依赖
- 测试代码不能占用过多Flash和RAM空间,避免影响正常业务功能
测试框架的选择
针对嵌入式场景,优先选择轻量级、无依赖或者依赖极少的C++测试框架,以下是两类常见选择:
无依赖的轻量框架
这类框架通常只有几个头文件,不需要链接额外的库,适合裸机或者资源极小的嵌入式场景,比如Unity测试框架,它是纯C实现的轻量测试框架,也可以兼容C++代码,核心代码量只有几千行,不依赖标准库,只需要实现几个底层的输出接口即可运行。
适配嵌入式的C++测试框架
如果嵌入式系统支持C++标准库的部分特性,可以选择Google Test的嵌入式适配版本,或者Catch2的轻量配置模式,通过关闭不必要的特性来减少资源占用。
硬件依赖隔离方法
嵌入式代码中的硬件操作通常通过寄存器访问、驱动接口调用实现,要测试这些代码的逻辑,需要先隔离硬件依赖,常用的方法是接口抽象和Mock:
接口抽象
将硬件相关的操作封装成抽象接口,业务逻辑只依赖接口而非具体硬件实现,测试时传入模拟的实现即可。例如外设串口发送功能的抽象:
// 串口发送抽象接口
class UartInterface {
public:
virtual ~UartInterface() = default;
// 发送数据接口
virtual void send(const uint8_t* data, uint16_t len) = 0;
};
// 真实硬件实现
class HardwareUart : public UartInterface {
public:
void send(const uint8_t* data, uint16_t len) override {
// 操作硬件寄存器发送数据
for (uint16_t i = 0; i < len; ++i) {
// 假设UART_TX_REG是硬件发送寄存器
*reinterpret_cast<volatile uint8_t*>(0x40001000) = data[i];
}
}
};
Mock实现
测试时创建接口的Mock实现,记录调用参数和次数,方便验证业务逻辑是否正确调用了硬件接口:
// 串口的Mock实现
class MockUart : public UartInterface {
public:
void send(const uint8_t* data, uint16_t len) override {
// 记录调用参数
call_count++;
sent_data.assign(data, data + len);
}
// 测试用辅助方法
uint32_t get_call_count() const { return call_count; }
const std::vector<uint8_t>& get_sent_data() const { return sent_data; }
private:
uint32_t call_count = 0;
std::vector<uint8_t> sent_data;
};
测试代码组织与示例
测试代码需要和正常业务代码分离,通常放在独立的test目录下,编译时通过条件编译或者不同的编译目标来控制是否包含测试代码。以下是一个完整的测试示例,使用轻量的测试宏实现断言:
#include <cstdint>
#include <vector>
#include <cstring>
// 简单测试断言宏
#define TEST_ASSERT(cond)
do {
if (!(cond)) {
/* 这里可以对接底层的输出接口,比如串口打印错误信息 */
test_fail_count++;
return;
}
} while(0)
// 前置声明的接口和Mock类(和上面的定义一致)
class UartInterface {
public:
virtual ~UartInterface() = default;
virtual void send(const uint8_t* data, uint16_t len) = 0;
};
class MockUart : public UartInterface {
public:
void send(const uint8_t* data, uint16_t len) override {
call_count++;
sent_data.assign(data, data + len);
}
uint32_t get_call_count() const { return call_count; }
const std::vector<uint8_t>& get_sent_data() const { return sent_data; }
private:
uint32_t call_count = 0;
std::vector<uint8_t> sent_data;
};
// 待测试的业务逻辑:数据打包后通过串口发送
class DataPacker {
public:
DataPacker(UartInterface* uart) : uart_(uart) {}
// 打包数据并发送,添加头部和校验
void pack_and_send(const uint8_t* payload, uint16_t payload_len) {
uint8_t buffer[256];
// 添加头部 0xAA 0xBB
buffer[0] = 0xAA;
buffer[1] = 0xBB;
// 拷贝 payload
memcpy(buffer + 2, payload, payload_len);
// 计算简单校验和
uint8_t checksum = 0;
for (uint16_t i = 0; i < payload_len; ++i) {
checksum += payload[i];
}
buffer[2 + payload_len] = checksum;
// 发送数据
uart_->send(buffer, 2 + payload_len + 1);
}
private:
UartInterface* uart_;
};
// 测试失败计数
uint32_t test_fail_count = 0;
// 测试用例:验证打包发送功能
void test_data_packer() {
MockUart mock_uart;
DataPacker packer(&mock_uart);
uint8_t payload[] = {0x01, 0x02, 0x03};
packer.pack_and_send(payload, 3);
// 断言调用次数
TEST_ASSERT(mock_uart.get_call_count() == 1);
// 断言发送数据长度
TEST_ASSERT(mock_uart.get_sent_data().size() == 6);
// 断言头部正确
TEST_ASSERT(mock_uart.get_sent_data()[0] == 0xAA);
TEST_ASSERT(mock_uart.get_sent_data()[1] == 0xBB);
// 断言 payload 正确
TEST_ASSERT(mock_uart.get_sent_data()[2] == 0x01);
TEST_ASSERT(mock_uart.get_sent_data()[3] == 0x02);
TEST_ASSERT(mock_uart.get_sent_data()[4] == 0x03);
// 断言校验和正确:0x01+0x02+0x03=0x06
TEST_ASSERT(mock_uart.get_sent_data()[5] == 0x06);
}
// 测试入口
int main() {
test_data_packer();
if (test_fail_count == 0) {
// 测试通过,可对接硬件指示
} else {
// 测试失败,可对接硬件指示
}
return 0;
}
测试执行与资源优化
在嵌入式设备上运行测试时,可以通过以下方式优化资源占用:
- 关闭测试框架的非必要特性,比如日志输出、异常支持等
- 测试代码使用静态分配内存,避免动态内存申请
- 将测试代码放在独立的Flash扇区,正式发布版本可以擦除该扇区减少固件体积
- 对于无输出设备的嵌入式板,可以通过LED闪烁、GPIO电平变化来指示测试结果
总结
嵌入式系统中的C++单元测试核心是先隔离硬件依赖,再选择轻量适配的测试框架,通过接口抽象和Mock技术让业务逻辑可以在脱离硬件的环境中被验证。合理的测试组织可以降低后期调试成本,提升代码的可靠性,即使资源受限的嵌入式场景也可以落地基础的单元测试能力。
embedded_C++unit_testtest_frameworkmock修改时间:2026-06-12 19:24:21