4. 第一个单元测试:使用CUnit编写简单的加法函数测试、编译与运行
好,咱们正式开始动手写第一个单元测试。
说实话,我第一次接触单元测试的时候,心里也犯嘀咕:这不就是多写一堆代码吗?后来踩过几次坑,才明白——测试不是负担,是救生圈。今天咱们就用CUnit这个轻量级框架,写一个最简单的加法函数测试。你跟着我走一遍,会发现其实没那么玄乎。
4.1 准备工作:安装CUnit
在开始之前,你得确保机器上装了CUnit。我习惯用Ubuntu,一条命令搞定:
sudo apt-get install libcunit1 libcunit1-doc libcunit1-dev
如果你是Mac用户,用Homebrew:
brew install cunit
Windows的话,我建议用Cygwin或者WSL。嗯,这里要注意:不同发行版的包名可能略有差异,但核心库就是libcunit。
装完之后,验证一下:
dpkg -l | grep cunit
看到版本号,就说明装好了。
4.2 被测函数:一个简单的加法
咱们先写一个要被测试的函数。我选加法,因为简单明了,方便你理解测试的套路。
新建一个文件 calc.c:
// calc.c
#include "calc.h"
int add(int a, int b) {
return a + b;
}
再建一个头文件 calc.h:
// calc.h
#ifndef CALC_H
#define CALC_H
int add(int a, int b);
#endif
你看,这个函数没什么花头。但就是这种简单的函数,测试起来最能说明问题。
4.3 编写测试用例:CUnit风格
接下来是重头戏——写测试。CUnit的测试用例,说白了就是一个个普通的C函数,只不过它们要遵循CUnit的注册规则。
新建 test_calc.c:
// test_calc.c
#include <CUnit/CUnit.h>
#include <CUnit/Basic.h>
#include "calc.h"
// 测试用例1:正数相加
void test_add_positive(void) {
CU_ASSERT(add(2, 3) == 5);
CU_ASSERT(add(0, 5) == 5);
CU_ASSERT(add(100, 200) == 300);
}
// 测试用例2:负数相加
void test_add_negative(void) {
CU_ASSERT(add(-1, -2) == -3);
CU_ASSERT(add(-5, 3) == -2);
CU_ASSERT(add(0, -5) == -5);
}
// 测试用例3:边界值
void test_add_boundary(void) {
CU_ASSERT(add(2147483647, 0) == 2147483647); // INT_MAX
CU_ASSERT(add(-2147483648, 0) == -2147483648); // INT_MIN
}
这里我用了CU_ASSERT宏。它的作用很简单:如果表达式为真,测试通过;否则失败。我在项目中遇到过不少新手把CU_ASSERT和assert搞混——前者是测试框架的断言,后者是C标准库的断言,用途完全不同。
4.4 注册测试套件与运行
光有测试函数还不够,你得把它们注册到CUnit的框架里。这就像搭积木:先建一个测试套件(Suite),再把测试用例(Test)放进去。
在 test_calc.c 里继续添加:
int main(void) {
CU_pSuite pSuite = NULL;
// 初始化CUnit测试注册器
if (CUE_SUCCESS != CU_initialize_registry())
return CU_get_error();
// 创建一个测试套件
pSuite = CU_add_suite("Suite_Add", NULL, NULL);
if (NULL == pSuite) {
CU_cleanup_registry();
return CU_get_error();
}
// 向套件中添加测试用例
if ((NULL == 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 add boundary", test_add_boundary))) {
CU_cleanup_registry();
return CU_get_error();
}
// 运行所有测试,使用Basic模式输出
CU_basic_set_mode(CU_BRM_VERBOSE);
CU_basic_run_tests();
CU_cleanup_registry();
return CU_get_error();
}
这段代码的流程很清晰:
- 初始化注册器
- 创建套件
- 添加测试用例
- 运行测试
- 清理资源
你想想看,如果每次都要手动写这么多模板代码,是不是很烦?我个人的习惯是写一个宏或者辅助函数来封装注册过程,但第一次学,还是老老实实走一遍流程比较好。
4.5 编译与运行
编译命令如下:
gcc -o test_calc test_calc.c calc.c -lcunit -I.
解释一下:
-o test_calc:输出可执行文件test_calc.c calc.c:源文件-lcunit:链接CUnit库-I.:当前目录包含头文件
运行:
./test_calc
你会看到类似这样的输出:
CUnit - A unit testing framework for C - Version 2.1-3
http://cunit.sourceforge.net/
Suite: Suite_Add
Test: test of add positive ... passed
Test: test of add negative ... passed
Test: test of add boundary ... passed
--Run Summary: Type Total Ran Passed Failed
suites 1 1 1 0
tests 3 3 3 0
asserts 8 8 8 0
看到三个passed,说明测试全部通过。如果某个测试失败,CUnit会明确告诉你哪个断言失败了,方便定位问题。
核心要点:CUnit的测试流程就是「注册-运行-清理」三步走。你只要记住这个模式,后面写再复杂的测试都不怕。
4.6 避坑指南
我曾经犯过的错:
- 忘记链接
-lcunit,编译报错说找不到CU_initialize_registry。嗯,这个错误很常见,检查链接库就行。 - 测试函数签名写错。CUnit要求测试函数必须是
void func(void)形式,多一个参数都不行。 - 忘记调用
CU_cleanup_registry。虽然小测试看不出来,但长期运行会有内存泄漏。
小技巧:如果你觉得每次编译命令太长,可以写个Makefile。我一般这样写:
test: test_calc.c calc.c
gcc -o $@ $^ -lcunit -I.
clean:
rm -f test_calc
然后直接make test就行了。
4.7 知识体系图
下面这张图帮你理清CUnit测试的核心逻辑:
4.8 小结
今天咱们走完了第一个CUnit测试的完整流程。从安装库、写被测函数,到编写测试用例、注册套件、编译运行,每一步我都尽量把坑点指出来。
你可能会觉得:就一个加法函数,折腾这么多代码,值得吗?我刚开始也有这个疑问。但后来项目越来越大,一个函数改动可能影响几十个模块——这时候,单元测试就是你的安全网。没有它,你改代码就像走钢丝。
下一节,咱们会深入CUnit的断言机制,看看除了CU_ASSERT之外,还有哪些更强大的断言工具。
公众号:蓝海资料掘金营,微信deep3321