单元测试:如何使用 CUnit 进行单元测试?
说实话,很多嵌入式工程师对单元测试的态度是「知道很重要,但就是懒得写」。我以前也是这样,总觉得嵌入式代码跑在硬件上,测不测无所谓。直到有一次,一个简单的 if 条件写反了,导致产线上批量烧录的板子有一半功能异常……嗯,从那以后,我再也不敢跳过单元测试了。
今天咱们聊聊 CUnit,一个轻量级的 C 语言单元测试框架。它不复杂,但用好了能帮你省下大量调试时间。
什么是 CUnit?
CUnit 是一个专门为 C 语言设计的单元测试框架。它的核心思路很简单:你写测试用例,它帮你自动运行并报告结果。说白了,就是把你手动验证代码的过程自动化了。
我个人习惯把 CUnit 和 assert 宏做对比:assert 只能告诉你「这里出错了」,而 CUnit 能告诉你「哪个测试用例失败了、期望值是多少、实际值是多少」。你想想看,哪个更适合做回归测试?
核心概念速览:
- 测试注册(Test Registry):管理所有测试套件和用例的容器
- 测试套件(Test Suite):一组相关的测试用例集合
- 测试用例(Test Case):单个测试函数,验证一个具体行为
- 断言宏(Assert Macros):判断测试是否通过的检查点
安装与配置
CUnit 的安装方式取决于你的开发环境。在 Linux 上,我一般直接用包管理器搞定:
# Ubuntu/Debian
sudo apt-get install libcunit1 libcunit1-dev
# CentOS/RHEL
sudo yum install cunit cunit-devel
# macOS (Homebrew)
brew install cunit
如果你在嵌入式交叉编译环境中使用,可能需要从源码编译。不过大多数情况下,用包管理器就够了。
小提示:编译时记得链接 -lcunit 库。我见过不少新手写了半天测试代码,结果编译时报「未定义引用」的错误,其实就是忘了加链接参数。
一个完整的测试示例
咱们直接上手写一个。假设你有一个计算器模块,包含加法和除法两个函数:
/* calculator.h */
#ifndef CALCULATOR_H
#define CALCULATOR_H
int add(int a, int b);
int divide(int a, int b, int *remainder);
#endif
/* calculator.c */
#include "calculator.h"
int add(int a, int b) {
return a + b;
}
int divide(int a, int b, int *remainder) {
if (b == 0) {
return 0; /* 除零错误 */
}
if (remainder) {
*remainder = a % b;
}
return a / b;
}
现在我们来写测试代码。这是 CUnit 测试文件的核心结构:
/* test_calculator.c */
#include <CUnit/CUnit.h>
#include <CUnit/Basic.h>
#include "calculator.h"
/* 测试用例:测试加法 */
void test_add_positive(void) {
CU_ASSERT(add(2, 3) == 5);
CU_ASSERT(add(0, 0) == 0);
CU_ASSERT(add(100, 200) == 300);
}
void test_add_negative(void) {
CU_ASSERT(add(-1, 1) == 0);
CU_ASSERT(add(-5, -3) == -8);
}
/* 测试用例:测试除法 */
void test_divide_normal(void) {
int rem;
int result = divide(10, 3, &rem);
CU_ASSERT(result == 3);
CU_ASSERT(rem == 1);
}
void test_divide_by_zero(void) {
int rem;
int result = divide(5, 0, &rem);
CU_ASSERT(result == 0); /* 期望返回0表示错误 */
}
/* 测试套件的初始化与清理 */
int init_suite(void) {
/* 这里可以做一些初始化工作,比如分配内存 */
return 0;
}
int clean_suite(void) {
/* 清理工作 */
return 0;
}
int main() {
CU_pSuite pSuite = NULL;
/* 1. 初始化测试注册器 */
if (CU_initialize_registry() != CUE_SUCCESS) {
return CU_get_error();
}
/* 2. 添加测试套件 */
pSuite = CU_add_suite("Calculator_Test_Suite", init_suite, clean_suite);
if (pSuite == NULL) {
CU_cleanup_registry();
return CU_get_error();
}
/* 3. 向套件中添加测试用例 */
if (CU_add_test(pSuite, "test of add positive", test_add_positive) == NULL ||
CU_add_test(pSuite, "test of add negative", test_add_negative) == NULL ||
CU_add_test(pSuite, "test of divide normal", test_divide_normal) == NULL ||
CU_add_test(pSuite, "test of divide by zero", test_divide_by_zero) == NULL) {
CU_cleanup_registry();
return CU_get_error();
}
/* 4. 运行所有测试(使用基本模式输出) */
CU_basic_set_mode(CU_BRM_VERBOSE);
CU_basic_run_tests();
/* 5. 清理 */
CU_cleanup_registry();
return 0;
}
编译运行:
gcc -o test_calculator test_calculator.c calculator.c -lcunit
./test_calculator
输出效果大概是这样:
CUnit - A unit testing framework for C - Version 2.1-3
http://cunit.sourceforge.net/
Suite: Calculator_Test_Suite
Test: test of add positive ... passed
Test: test of add negative ... passed
Test: test of divide normal ... passed
Test: test of divide by zero ... passed
--Run Summary: Type Total Ran Passed Failed
suites 1 1 1 0
tests 4 4 4 0
asserts 10 10 10 0
常用的断言宏
CUnit 提供了丰富的断言宏。我整理了一份常用清单:
| 宏名称 | 用途 | 示例 |
|---|---|---|
CU_ASSERT(expr) |
判断表达式为真 | CU_ASSERT(ptr != NULL); |
CU_ASSERT_EQUAL(actual, expected) |
判断两个整数相等 | CU_ASSERT_EQUAL(result, 42); |
CU_ASSERT_DOUBLE_EQUAL(actual, expected, epsilon) |
判断浮点数近似相等 | CU_ASSERT_DOUBLE_EQUAL(pi, 3.14159, 0.0001); |
CU_ASSERT_STRING_EQUAL(actual, expected) |
判断字符串相等 | CU_ASSERT_STRING_EQUAL(name, "Alice"); |
CU_ASSERT_PTR_EQUAL(actual, expected) |
判断指针相等 | CU_ASSERT_PTR_EQUAL(p, &buffer[0]); |
CU_ASSERT_PTR_NULL(ptr) |
判断指针为 NULL | CU_ASSERT_PTR_NULL(freed_ptr); |
CU_FAIL(message) |
强制标记测试失败 | CU_FAIL("不应该执行到这里"); |
注意:浮点数比较千万别直接用 CU_ASSERT_EQUAL。我曾经在一个项目中看到有人用 CU_ASSERT_EQUAL(1.0/3.0, 0.333333),结果测试时有时通过有时失败,折腾了半天才发现是浮点精度问题。用 CU_ASSERT_DOUBLE_EQUAL 加上合适的 epsilon 才是正道。
测试运行模式
CUnit 支持三种输出模式,我一般根据场景切换:
- CU_BRM_NORMAL:只显示失败信息,适合快速检查
- CU_BRM_VERBOSE:显示所有测试的详细信息,适合调试
- CU_BRM_SILENT:不输出到控制台,适合自动化脚本
设置方式很简单:
CU_basic_set_mode(CU_BRM_VERBOSE); /* 或者 NORMAL / SILENT */
嵌入式环境中的注意事项
在嵌入式开发中跑单元测试,有几个坑我得提醒你:
- 硬件依赖:如果你的代码直接操作寄存器或外设,单元测试在 PC 上跑不了。我习惯把硬件抽象层(HAL)抽出来,测试时用 mock 替换。
- 内存限制:嵌入式设备内存小,但 CUnit 本身很轻量,几百字节就能跑。不过别在目标板上跑大量测试,建议在开发主机上交叉编译测试。
- 交叉编译:用你的交叉编译器编译测试程序,然后在目标板上运行。或者用 QEMU 模拟运行。
我的经验:在嵌入式项目中,我通常把单元测试放在 CI 流程中。每次提交代码,CI 服务器用 x86 环境编译并运行测试。这样既能快速发现问题,又不用依赖硬件。只有通过了单元测试的代码,才会进入硬件集成测试阶段。
总结
CUnit 虽然功能不算花哨,但胜在简单可靠。你只需要掌握三个步骤:注册套件、添加用例、运行测试。剩下的就是写断言了。
说实话,写单元测试一开始会觉得麻烦,但当你改完一个函数,跑一下测试发现所有用例都通过时,那种踏实感是调试器给不了的。尤其是做回归测试,改完代码一键验证,比手动点来点去高效太多了。
嗯,今天就聊到这儿。下次你写 C 代码的时候,不妨试试给核心函数加上 CUnit 测试。相信我,你会爱上这种「代码改完心里有底」的感觉。