第二章 测试框架入门:CUnit框架介绍、安装与配置、第一个测试用例

好,咱们进入正题。上一章聊了聊单元测试到底是个啥,为什么值得你花时间。这一章,咱们直接上手——用CUnit这个框架,写你的第一个测试用例。

说实话,我刚开始接触单元测试的时候,也看过不少框架。什么Check、Unity、Criterion,各有各的脾气。但CUnit是我个人用得最顺手的一个。为什么?因为它够简单,够直观,而且文档写得还算清楚。你想想看,咱们搞嵌入式的人,时间本来就紧,哪有功夫去研究那些花里胡哨的框架配置?

2.1 CUnit是什么?

CUnit,说白了就是一个轻量级的C语言单元测试框架。它跟JUnit是亲戚——设计思路很像,都是把测试用例组织成“套件(Suite)”,然后批量跑。

它的核心概念其实就三个:

  • 测试注册表(Test Registry):管理所有测试套件的地方。就像一个目录,告诉你有哪些测试可以跑。
  • 测试套件(Test Suite):一组相关的测试用例的集合。比如你写了一个计算器模块,那加减乘除的测试就可以放在一个套件里。
  • 测试用例(Test Case):最小的测试单元。一个用例只测一件事,比如“测试加法函数输入2+3是否等于5”。

嗯,这里要注意:CUnit支持两种运行模式——自动模式和交互模式。自动模式适合集成到构建脚本里,跑完就出结果;交互模式适合调试时用,可以一步步看。我个人习惯用自动模式,省事。

2.2 安装与配置

安装CUnit其实不复杂,但不同平台有点小区别。我分别说一下。

Linux环境(Ubuntu/Debian)

如果你用的是Ubuntu,直接一条命令搞定:

sudo apt-get install libcunit1 libcunit1-dev

装完之后,头文件在 /usr/include/CUnit/,库文件在 /usr/lib/x86_64-linux-gnu/。编译的时候记得链接 -lcunit

举个例子,编译命令长这样:

gcc -o test_calc test_calc.c -lcunit

Windows环境(MinGW)

Windows下稍微麻烦一点。我建议用MinGW或者Cygwin。如果你用MinGW,可以去CUnit官网下载源码包,然后手动编译:

./configure --prefix=/usr/local
make
make install

我曾经在Windows上折腾过一整个下午,就为了把CUnit跑起来。后来发现其实有个更简单的办法——直接用vcpkg:

vcpkg install cunit

嗯,省心多了。

验证安装

装完之后,你可以写个最简单的程序验证一下:

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

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

如果能编译通过并运行,说明安装成功了。

2.3 第一个测试用例

好,重头戏来了。咱们写一个真正的测试用例。假设你有一个计算器模块,里面有个加法函数:

// calc.h
#ifndef CALC_H
#define CALC_H

int add(int a, int b);

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

int add(int a, int b) {
    return a + b;
}

现在,我们来写测试代码。完整的测试文件长这样:

// 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(-2, -3) == -5);
    CU_ASSERT(add(-5, 0) == -5);
}

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

int main() {
    // 初始化测试注册表
    if (CU_initialize_registry() != CUE_SUCCESS) {
        return CU_get_error();
    }

    // 创建测试套件
    CU_pSuite pSuite = CU_add_suite("Suite_Calculator", NULL, NULL);
    if (pSuite == NULL) {
        CU_cleanup_registry();
        return CU_get_error();
    }

    // 向套件中添加测试用例
    if (CU_add_test(pSuite, "test of add positive", test_add_positive) == NULL) {
        CU_cleanup_registry();
        return CU_get_error();
    }
    if (CU_add_test(pSuite, "test of add negative", test_add_negative) == NULL) {
        CU_cleanup_registry();
        return CU_get_error();
    }
    if (CU_add_test(pSuite, "test of add boundary", test_add_boundary) == NULL) {
        CU_cleanup_registry();
        return CU_get_error();
    }

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

    return 0;
}

编译运行:

gcc -o test_calc test_calc.c calc.c -lcunit
./test_calc

如果一切顺利,你会看到类似这样的输出:

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

Suite: Suite_Calculator
  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     n/a       0
               tests         3       3       3       0
               asserts       7       7       7       0

看到“passed”三个字,心里是不是踏实多了?

核心要点:一个测试用例只测一件事。别想着一个函数测所有情况,拆开写,出了问题一眼就能定位。

2.4 常用断言宏

CUnit提供了一堆断言宏,我挑几个最常用的列出来:

作用
CU_ASSERT(expr) 判断表达式是否为真
CU_ASSERT_EQUAL(actual, expected) 判断两个值是否相等
CU_ASSERT_NOT_EQUAL(actual, expected) 判断两个值是否不相等
CU_ASSERT_PTR_NULL(ptr) 判断指针是否为NULL
CU_ASSERT_PTR_NOT_NULL(ptr) 判断指针是否不为NULL
CU_ASSERT_STRING_EQUAL(str1, str2) 判断两个字符串是否相等

我个人用得最多的是 CU_ASSERT_EQUALCU_ASSERT。前者适合数值比较,后者适合布尔判断。记住一点:断言失败时,CUnit会继续执行当前用例中的后续断言,不会立即终止。这一点跟某些框架不一样,你得心里有数。

小技巧:如果你想让测试在第一个断言失败时就停下来,可以用 CU_ASSERT_FATAL 系列宏。不过一般情况下,我建议让测试跑完,这样你能看到所有失败点。

2.5 避坑指南

嗯,这里说几个我踩过的坑:

  • 忘记初始化注册表CU_initialize_registry() 必须在所有操作之前调用。我曾经忘了这步,结果程序直接崩溃,查了半天才发现。
  • 套件名和用例名重复:CUnit内部用名字来标识套件和用例,如果重名,后面的会覆盖前面的。所以命名时最好有点规律,比如“Suite_模块名”、“test_函数名_场景”。
  • 链接顺序问题:编译时 -lcunit 要放在源文件后面。有些编译器对链接顺序敏感,放前面可能报未定义引用。

警告:不要在测试用例里写 printf 来调试!用断言就够了。如果非要用,记得在正式提交前删掉,否则输出会乱成一团。

2.6 知识体系图

为了让你对整个流程有个直观的认识,我画了一张图:

CUnit 测试框架核心流程 测试注册表 测试套件 测试用例 断言宏 测试报告 CU_initialize_registry() CU_add_suite() CU_add_test() CU_ASSERT_xxx() CU_basic_run_tests() 管理所有套件 组织相关用例 单个测试函数 验证结果 输出通过/失败

这张图把整个流程串起来了。你写测试的时候,就按这个顺序来:初始化注册表 → 创建套件 → 添加用例 → 写断言 → 跑测试看报告。一步都不能少。

2.7 小结

这一章咱们把CUnit的安装、配置和第一个测试用例都过了一遍。说实话,写测试用例这件事,刚开始会觉得有点麻烦——明明代码跑得好好的,干嘛要多写一堆测试代码?但等你真正遇到那种“改了一行代码,炸了三个模块”的情况时,你就会感谢当初写了测试的自己。

下一章,咱们会深入聊聊测试用例的设计技巧,包括怎么划分测试粒度、怎么处理依赖关系。嗯,到时候见。


公众号:蓝海资料掘金营,微信deep3321