第3章:CUnit框架入门

好,咱们今天来聊聊CUnit。说实话,在嵌入式C的世界里,做单元测试的框架就那么几个。CUnit算是其中比较轻量、容易上手的一个。我个人习惯把它叫做「C语言界的JUnit」——虽然功能没那么花哨,但该有的都有了。

3.1 CUnit的安装与配置

先说说安装。这事儿其实不复杂,但我在项目中遇到过几次因为版本不对导致编译失败的情况,所以咱们一步步来。

3.1.1 下载与编译

CUnit的源码可以从SourceForge上拿到。你直接搜「CUnit SourceForge」就行。下载下来是个压缩包,解压后进入目录,标准的Linux三板斧:

./configure
make
sudo make install

嗯,这里要注意。如果你是在嵌入式Linux环境下,可能需要指定交叉编译工具链。比如:

./configure --host=arm-linux-gnueabihf --prefix=/usr/local/arm/cunit

为什么要指定prefix?说白了,就是不想让库文件污染系统目录。我一般习惯把第三方库都装到单独的目录里,方便管理。

3.1.2 验证安装

装完之后,怎么知道装好了?写个小程序试试:

#include <CUnit/CUnit.h>
#include <CUnit/Basic.h>

int main() {
    if (CU_initialize_registry() != CUE_SUCCESS) {
        return CU_get_error();
    }
    CU_cleanup_registry();
    return 0;
}

编译命令:

gcc -o test_cunit test_cunit.c -lcunit -lcunitbasic

如果能编译通过并正常运行,恭喜你,环境搭好了。

注意:Windows环境下,CUnit的安装稍微麻烦点。我建议直接用MinGW或者Cygwin。曾经有个同事在Windows上折腾了一整天,最后发现是路径里有中文空格——嗯,这种坑我踩过不止一次。

3.2 CUnit的基本结构:Suite与Test

理解了安装,咱们来看看CUnit的核心概念。说白了就两个东西:SuiteTest

  • Suite(测试套件):一组相关的测试用例的集合。比如你有个模块叫「温度传感器驱动」,那它的所有测试用例就放在一个Suite里。
  • Test(测试用例):一个具体的测试函数。比如「测试温度读取函数在正常范围内是否返回正确值」。

你想想看,这就像整理文件。Suite是文件夹,Test是里面的文件。没有文件夹,文件就乱成一团;没有文件,文件夹就是个空壳。

3.2.1 Suite的创建与注册

在CUnit里,创建Suite的流程是这样的:

CU_pSuite pSuite = NULL;

// 第一步:初始化注册表
if (CUE_SUCCESS != CU_initialize_registry())
    return CU_get_error();

// 第二步:创建Suite
pSuite = CU_add_suite("Suite_温度传感器", init_suite, clean_suite);
if (NULL == pSuite) {
    CU_cleanup_registry();
    return CU_get_error();
}

这里有个细节:init_suiteclean_suite是两个回调函数。它们分别在Suite开始前和结束后被调用。我一般用init_suite来分配资源、初始化硬件模拟,用clean_suite来释放资源。

小技巧:如果你的测试不需要初始化和清理,直接传NULL就行。CUnit会帮你跳过这步。

3.2.2 Test的添加

有了Suite,接下来就是往里加Test:

CU_add_test(pSuite, "测试正常温度读取", test_temp_normal);
CU_add_test(pSuite, "测试温度上限溢出", test_temp_overflow);
CU_add_test(pSuite, "测试温度传感器断开", test_temp_disconnect);

每个测试函数都是一个普通的C函数,返回值必须是void,不接受参数:

void test_temp_normal(void) {
    // 测试代码
}

3.3 编写第一个CUnit测试用例

光说不练假把式。咱们写一个完整的例子。假设我们有个简单的加法函数:

// 被测试的函数
int add(int a, int b) {
    return a + b;
}

现在来写测试:

#include <CUnit/CUnit.h>
#include <CUnit/Basic.h>

// 测试用例1:正数相加
void test_add_positive(void) {
    CU_ASSERT(add(2, 3) == 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);
}

// 测试用例3:边界值
void test_add_boundary(void) {
    CU_ASSERT(add(0, 0) == 0);
    CU_ASSERT(add(INT_MAX, 0) == INT_MAX);
    CU_ASSERT(add(INT_MIN, 0) == INT_MIN);
}

int main() {
    // 初始化
    if (CUE_SUCCESS != CU_initialize_registry())
        return CU_get_error();

    // 创建Suite
    CU_pSuite pSuite = CU_add_suite("Suite_加法测试", NULL, NULL);
    if (NULL == pSuite) {
        CU_cleanup_registry();
        return CU_get_error();
    }

    // 添加测试用例
    CU_add_test(pSuite, "测试正数相加", test_add_positive);
    CU_add_test(pSuite, "测试负数相加", test_add_negative);
    CU_add_test(pSuite, "测试边界值", test_add_boundary);

    // 运行测试
    CU_basic_set_mode(CU_BRM_VERBOSE);
    CU_basic_run_tests();
    CU_cleanup_registry();

    return 0;
}

这段代码里,我用了CU_ASSERT宏。它和标准C的assert类似,但CUnit会记录断言的结果,而不是直接崩溃。你想想看,如果测试一个挂一个,那调试起来得多痛苦?

核心要点:CUnit提供了多种断言宏,比如CU_ASSERT、CU_ASSERT_EQUAL、CU_ASSERT_TRUE等。我个人建议尽量用带具体比较的宏,这样测试失败时你能一眼看出是哪个值不对。

3.4 运行测试并查看结果

代码写好了,怎么跑?编译运行就行:

gcc -o test_add test_add.c -lcunit -lcunitbasic
./test_add

输出结果大概长这样:

     CUnit - A Unit testing framework for C - Version 2.1-3
     http://cunit.sourceforge.net/


Suite: Suite_加法测试
  Test: 测试正数相加 ... passed
  Test: 测试负数相加 ... passed
  Test: 测试边界值 ... passed

--Run Summary: Type      Total     Ran  Passed  Failed
               suites          1       1     n/a       0
               tests           3       3       3       0
               asserts         7       7       7       0

看到「passed」是不是很爽?但别高兴太早。我曾经在项目里遇到过测试全绿,结果上线后功能还是崩了的情况。为什么?因为测试用例覆盖不全。你想想看,如果只测了正数相加,没测溢出情况,那INT_MAX+1这种场景就漏掉了。

3.4.1 测试失败时的输出

如果某个测试失败了,CUnit会告诉你具体是哪一行:

Suite: Suite_加法测试
  Test: 测试边界值 ... FAILED
    1. test_add.c:23  - CU_ASSERT(add(INT_MAX, 1) == INT_MIN)

--Run Summary: Type      Total     Ran  Passed  Failed
               suites          1       1     n/a       0
               tests           3       3       2       1
               asserts         7       7       6       1

看到第23行了吗?这就是CUnit的贴心之处——直接告诉你断言失败的位置。省得你一行行去翻代码。

调试技巧:如果测试失败,别急着改代码。先确认你的测试用例本身是不是写错了。我曾经有一次花了两小时查bug,最后发现是测试用例里的期望值写反了——嗯,这种低级错误谁都会犯。

3.5 小结

这一章咱们走了一遍CUnit的完整流程:从安装配置,到理解Suite和Test的概念,再到写第一个测试用例,最后看测试结果。说白了,CUnit就是个工具,真正重要的是你写测试用例的思路。

下一章咱们会深入聊聊CUnit的断言宏和测试夹具,这些东西在实际项目中特别有用。到时候我会分享一些我在嵌入式项目里踩过的坑,保证让你少走弯路。