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_ASSERTassert搞混——前者是测试框架的断言,后者是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();
}

这段代码的流程很清晰:

  1. 初始化注册器
  2. 创建套件
  3. 添加测试用例
  4. 运行测试
  5. 清理资源

你想想看,如果每次都要手动写这么多模板代码,是不是很烦?我个人的习惯是写一个宏或者辅助函数来封装注册过程,但第一次学,还是老老实实走一遍流程比较好。

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测试的核心逻辑:

CUnit 单元测试核心流程 1. 初始化注册器 CU_initialize_registry() 2. 创建测试套件 CU_add_suite() 3. 添加测试用例 CU_add_test() 4. 运行测试 CU_basic_run_tests() 5. 清理资源 CU_cleanup_registry() 6. 输出结果 passed / failed 统计 每个步骤对应一个CUnit API调用 测试用例可以包含多个 CU_ASSERT 断言

4.8 小结

今天咱们走完了第一个CUnit测试的完整流程。从安装库、写被测函数,到编写测试用例、注册套件、编译运行,每一步我都尽量把坑点指出来。

你可能会觉得:就一个加法函数,折腾这么多代码,值得吗?我刚开始也有这个疑问。但后来项目越来越大,一个函数改动可能影响几十个模块——这时候,单元测试就是你的安全网。没有它,你改代码就像走钢丝。

下一节,咱们会深入CUnit的断言机制,看看除了CU_ASSERT之外,还有哪些更强大的断言工具。


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