【C语言实战(79)】深入C语言单元测试:基于CUnit框架的实战指南

目录

  • 一、单元测试基础概念
    • [1.1 单元测试定义](#1.1 单元测试定义)
    • [1.2 单元测试的优势](#1.2 单元测试的优势)
    • [1.3 CUnit 框架简介](#1.3 CUnit 框架简介)
  • [二、CUnit 环境搭建与基础使用](#二、CUnit 环境搭建与基础使用)
    • [2.1 环境搭建(Linux)](#2.1 环境搭建(Linux))
      • [2.1.1 安装 CUnit 库](#2.1.1 安装 CUnit 库)
      • [2.1.2 编译命令](#2.1.2 编译命令)
    • [2.2 环境搭建(Windows)](#2.2 环境搭建(Windows))
      • [2.2.1 下载 CUnit 源码并编译](#2.2.1 下载 CUnit 源码并编译)
      • [2.2.2 Visual Studio 中配置](#2.2.2 Visual Studio 中配置)
    • [2.3 CUnit 基础流程](#2.3 CUnit 基础流程)
      • [2.3.1 定义测试函数与断言](#2.3.1 定义测试函数与断言)
      • [2.3.2 添加测试函数到测试套件](#2.3.2 添加测试函数到测试套件)
  • 三、单元测试实战
    • [3.1 基础函数测试](#3.1 基础函数测试)
      • [3.1.1 二分查找函数测试](#3.1.1 二分查找函数测试)
      • [3.1.2 结构体数组排序函数测试](#3.1.2 结构体数组排序函数测试)
    • [3.2 复杂模块测试](#3.2 复杂模块测试)
      • [3.2.1 学生成绩管理模块测试](#3.2.1 学生成绩管理模块测试)
      • [3.2.2 文件加密模块测试](#3.2.2 文件加密模块测试)
    • [3.3 测试报告分析](#3.3 测试报告分析)
      • [3.3.1 生成 HTML 报告](#3.3.1 生成 HTML 报告)
      • [3.3.2 持续集成思路](#3.3.2 持续集成思路)
        • [3.3.2.1 核心前提:将单元测试集成到 CMake 构建流程​](#3.3.2.1 核心前提:将单元测试集成到 CMake 构建流程)
        • [3.3.2.2 进阶:对接常用 CI 平台实现 "提交即测试"​](#3.3.2.2 进阶:对接常用 CI 平台实现 “提交即测试”)
        • [3.3.2.3 持续集成的维护要点​](#3.3.2.3 持续集成的维护要点)

一、单元测试基础概念

1.1 单元测试定义

单元测试是软件开发过程中不可或缺的一环,它专注于对代码中最小功能单元进行验证。在 C 语言中,这些最小功能单元通常指的就是函数。单元测试的核心任务是针对函数给定各种不同的输入,然后检查其输出是否与预先设定的预期结果一致。通过这样的方式,能够精确地验证每个函数的功能是否正确实现,确保代码在各种场景下的稳定性和可靠性。

例如,对于一个简单的加法函数int add(int a, int b),我们可以编写单元测试用例,输入不同的整数组合,如add(1, 2)、add(-1, 1)等,验证其返回值是否分别为 3 和 0,以此来确认函数的正确性。

1.2 单元测试的优势

  • 提前发现函数级错误:在软件开发过程中,越早发现错误,修复的成本就越低。单元测试允许开发人员在编写完函数后立即进行测试,能够快速捕捉到函数实现中的逻辑错误、边界条件处理不当等问题,避免这些问题在后续的集成测试或系统测试阶段才被发现,从而节省大量的调试时间和成本。
  • 支持代码重构:随着项目的发展,代码可能需要进行重构以提高可维护性、性能或扩展性。拥有完善的单元测试,开发人员在重构代码时就可以更加放心,因为重构后只需重新运行单元测试,如果测试全部通过,就可以基本确定重构后的代码功能没有受到影响。例如,将一个复杂的函数拆分成多个小函数,或者优化算法实现,只要单元测试结果不变,就说明代码的外部行为没有改变。
  • 降低集成测试难度:如果每个函数都经过了充分的单元测试,那么在进行集成测试时,就可以将重点放在模块之间的交互和接口上,而不必花费大量时间去排查单个函数的问题。这使得集成测试能够更加高效地进行,快速发现并解决模块集成过程中出现的问题,提高整个系统的稳定性和可靠性。

1.3 CUnit 框架简介

CUnit 是 C 语言领域中广泛使用的单元测试框架,它为 C 语言开发者提供了一套便捷、高效的单元测试解决方案。该框架支持多种灵活的测试套件组织方式,可以根据项目的结构和功能模块,将相关的测试用例组织成不同的测试套件,方便管理和执行。

同时,CUnit 具备强大的测试报告生成功能,能够生成文本格式和 HTML 格式的测试报告。文本报告简洁明了,适合在命令行环境下查看测试结果;HTML 报告则更加直观、详细,通过可视化的方式展示测试通过率、失败用例的具体信息,包括失败的测试函数、预期结果与实际结果的对比等,方便开发人员快速定位和解决问题。这使得开发团队在项目开发和维护过程中,能够清晰地了解代码的质量状况,及时发现并解决潜在的问题,从而提高项目的整体质量和稳定性。

二、CUnit 环境搭建与基础使用

2.1 环境搭建(Linux)

2.1.1 安装 CUnit 库

在基于 Debian 或 Ubuntu 的 Linux 系统中,安装 CUnit 库非常简单,只需在终端中执行以下命令:

c 复制代码
sudo apt-get install libcunit1 libcunit1-dev libcunit1-doc

这个命令会自动从软件源中下载并安装 CUnit 库的运行时文件(libcunit1)、开发文件(libcunit1-dev,包含头文件等,用于编译测试程序)以及文档(libcunit1-doc,方便开发者查阅相关资料)。

2.1.2 编译命令

当安装好 CUnit 库后,编译包含 CUnit 测试的程序时,需要使用gcc编译器,并指定链接 CUnit 库。假设我们有一个测试程序test_program.c,编译命令如下:

c 复制代码
gcc test_program.c -o test_program -lcunit -g

其中,test_program.c是测试程序的源文件;-o test_program指定生成的可执行文件名为test_program;-lcunit告诉编译器链接 CUnit 库,这样程序在运行时才能找到并使用 CUnit 库提供的功能;-g选项用于在生成的可执行文件中包含调试信息,方便在调试过程中查看程序的执行状态和变量值,帮助开发者定位问题。

2.2 环境搭建(Windows)

2.2.1 下载 CUnit 源码并编译

  1. 首先,从 CUnit 的官方网站(如 SourceForge 上的 CUnit 项目页面:http://sourceforge.net/projects/cunit )下载 CUnit 的源代码压缩包。
  2. 解压下载的压缩包到一个指定的目录,例如C:\CUnit。
  3. 安装 MinGW,它是一个在 Windows 平台上使用的 GCC 编译器集合。安装完成后,确保 MinGW 的bin目录(例如C:\MinGW\bin)已经添加到系统的环境变量PATH中,这样在命令行中就可以直接使用 MinGW 提供的编译工具。
  4. 打开命令提示符,进入到 CUnit 源代码解压后的目录(C:\CUnit)。
  5. 在命令提示符中执行编译命令,生成静态库libcunit.a:
c 复制代码
mingw32-make -f Makefile.win

这个命令会根据Makefile.win文件中的规则,使用 MinGW 的编译器对 CUnit 源代码进行编译,并生成静态库文件libcunit.a。编译过程中,如果遇到缺少依赖库或其他错误,需要根据错误提示进行相应的处理,例如安装缺少的库或者更新编译器版本等。

2.2.2 Visual Studio 中配置

  1. 配置库目录:打开 Visual Studio,创建一个新的项目或者打开已有的项目。在项目资源管理器中,右键点击项目名称,选择 "属性"。在弹出的属性页面中,依次展开 "配置属性" -> "链接器" -> "常规",在 "附加库目录" 中添加 CUnit 静态库所在的目录,即C:\CUnit(假设解压后的 CUnit 目录为C:\CUnit)。这个步骤告诉链接器在链接时去哪里查找 CUnit 的静态库文件。
  2. 配置头文件目录:在属性页面中,依次展开 "配置属性" -> "C/C++" -> "常规",在 "附加包含目录" 中添加 CUnit 头文件所在的目录,同样是C:\CUnit\include。这样,编译器在编译源文件时,就能够找到 CUnit 的头文件,从而正确解析和编译使用 CUnit 的代码。
  3. 链接静态库:在属性页面中,展开 "配置属性" -> "链接器" -> "输入",在 "附加依赖项" 中添加libcunit.a。这一步确保链接器在链接阶段将 CUnit 静态库与项目进行链接,使程序能够使用 CUnit 提供的功能。完成上述配置后,点击 "确定" 保存设置,就可以在 Visual Studio 项目中使用 CUnit 进行单元测试了。

2.3 CUnit 基础流程

2.3.1 定义测试函数与断言

在 CUnit 中,定义测试函数时,函数的返回类型必须为void,且没有参数。例如,我们要测试一个计算平均值的函数float calculate_average(int *scores, int count),可以定义如下测试函数:

c 复制代码
#include <CUnit/CUnit.h>

// 假设被测试的函数
float calculate_average(int *scores, int count) {
    int sum = 0;
    for (int i = 0; i < count; i++) {
        sum += scores[i];
    }
    return (float)sum / count;
}

// 测试函数
void test_calculate_average(void) {
    int scores[] = {80, 90, 85, 95, 85};
    int count = sizeof(scores) / sizeof(scores[0]);
    float average = calculate_average(scores, count);
    // 使用断言验证结果
    CU_ASSERT_EQUAL(average, 87);
}

在上述代码中,test_calculate_average函数是一个 CUnit 测试函数。首先,它调用被测试的calculate_average函数计算平均值,然后使用 CUnit 提供的断言宏CU_ASSERT_EQUAL来验证计算结果是否与预期值相等。CU_ASSERT_EQUAL宏接受两个参数,第一个参数是实际计算得到的值,第二个参数是预期值。如果这两个值相等,测试通过;否则,测试失败,CUnit 会记录下这个失败的测试用例,并在测试结束后报告相关信息。

2.3.2 添加测试函数到测试套件

  1. 添加测试函数到测试套件:在定义好测试函数后,需要将其添加到测试套件中。测试套件是一组相关测试函数的集合,方便组织和管理测试用例。以下是将前面定义的test_calculate_average函数添加到测试套件的代码示例:
c 复制代码
#include <CUnit/Basic.h>

// 测试函数
void test_calculate_average(void);

int main() {
    CU_pSuite pSuite = NULL;

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

    // 添加一个测试套件
    pSuite = CU_add_suite("Average Test Suite", NULL, NULL);
    if (NULL == pSuite) {
        CU_cleanup_registry();
        return CU_get_error();
    }

    // 添加测试函数到测试套件
    if (NULL == CU_add_test(pSuite, "Test calculate average", test_calculate_average)) {
        CU_cleanup_registry();
        return CU_get_error();
    }

    // 运行所有测试
    CU_basic_set_mode(CU_BRM_VERBOSE);
    CU_basic_run_tests();

    // 清理测试注册表
    CU_cleanup_registry();
    return CU_get_error();
}

在这段代码中,首先使用CU_initialize_registry函数初始化 CUnit 的测试注册表,这是使用 CUnit 的基础,必须在其他操作之前进行。然后,使用CU_add_suite函数创建一个新的测试套件,该函数接受三个参数:测试套件的名称(这里是 "Average Test Suite")、初始化函数(这里为NULL,表示不需要额外的初始化操作)和清理函数(这里也为NULL,表示不需要额外的清理操作)。接着,使用CU_add_test函数将测试函数test_calculate_average添加到刚刚创建的测试套件中,CU_add_test函数接受三个参数:测试套件指针、测试用例描述(这里是 "Test calculate average")和测试函数指针。

  1. 初始化 CUnit 框架:CU_initialize_registry函数用于初始化 CUnit 的内部数据结构,为后续的测试操作做准备。如果初始化失败,它会返回一个错误代码,程序可以根据这个错误代码进行相应的处理,例如输出错误信息并退出。

  2. 运行测试:在添加完所有测试函数到测试套件后,使用CU_basic_set_mode函数设置测试运行模式,这里设置为CU_BRM_VERBOSE,表示以详细模式运行测试,在测试过程中会输出更多的信息,方便开发者了解测试的执行情况。然后,使用CU_basic_run_tests函数运行所有已添加到测试注册表中的测试套件和测试用例。

  3. 清理资源:测试完成后,使用CU_cleanup_registry函数清理测试注册表,释放 CUnit 在测试过程中分配的资源,避免内存泄漏等问题。最后,返回CU_get_error函数的结果,这个结果表示测试过程中是否发生了错误,如果返回值为 0,表示测试全部通过;否则,表示有测试失败或发生了其他错误 。

三、单元测试实战

3.1 基础函数测试

3.1.1 二分查找函数测试

二分查找是一种在有序数组中查找特定元素的高效算法。其基本思想是将数组分成两部分,通过比较中间元素与目标元素的大小,不断缩小查找范围,直到找到目标元素或确定目标元素不存在。以下是二分查找函数的实现及针对不同场景的测试代码:

c 复制代码
#include <CUnit/CUnit.h>
#include <CUnit/Basic.h>

// 二分查找函数
int binary_search(int arr[], int size, int target) {
    int left = 0;
    int right = size - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (arr[mid] == target) {
            return mid;
        } else if (arr[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return -1;
}

// 测试查找存在的元素
void test_binary_search_exist(void) {
    int arr[] = {1, 3, 5, 7, 9};
    int size = sizeof(arr) / sizeof(arr[0]);
    int target = 5;
    int result = binary_search(arr, size, target);
    CU_ASSERT_EQUAL(result, 2);
}

// 测试查找不存在的元素
void test_binary_search_not_exist(void) {
    int arr[] = {1, 3, 5, 7, 9};
    int size = sizeof(arr) / sizeof(arr[0]);
    int target = 4;
    int result = binary_search(arr, size, target);
    CU_ASSERT_EQUAL(result, -1);
}

// 测试空数组
void test_binary_search_empty_array(void) {
    int arr[] = {};
    int size = sizeof(arr) / sizeof(arr[0]);
    int target = 5;
    int result = binary_search(arr, size, target);
    CU_ASSERT_EQUAL(result, -1);
}

// 测试单个元素数组
void test_binary_search_single_element_array(void) {
    int arr[] = {5};
    int size = sizeof(arr) / sizeof(arr[0]);
    int target = 5;
    int result = binary_search(arr, size, target);
    CU_ASSERT_EQUAL(result, 0);
}

在上述代码中,binary_search函数实现了二分查找功能。针对不同的测试场景,分别定义了test_binary_search_exist、test_binary_search_not_exist、test_binary_search_empty_array和test_binary_search_single_element_array四个测试函数。每个测试函数通过调用binary_search函数,并使用CU_ASSERT_EQUAL断言来验证函数的返回值是否符合预期。

3.1.2 结构体数组排序函数测试

在实际应用中,经常需要对结构体数组进行排序。下面以一个学生结构体数组为例,使用qsort函数对其按成绩进行排序,并编写相应的测试用例:

c 复制代码
#include <CUnit/CUnit.h>
#include <CUnit/Basic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 定义学生结构体
typedef struct {
    char name[50];
    int id;
    float score;
} Student;

// 比较函数,按成绩升序排列
int compare_students_by_score(const void *a, const void *b) {
    const Student *student_a = (const Student *) a;
    const Student *student_b = (const Student *) b;
    if (student_a->score < student_b->score) return -1;
    if (student_a->score > student_b->score) return 1;
    return 0;
}

// 比较函数,按成绩降序排列
int compare_students_by_score_descending(const void *a, const void *b) {
    const Student *student_a = (const Student *) a;
    const Student *student_b = (const Student *) b;
    if (student_a->score > student_b->score) return -1;
    if (student_a->score < student_b->score) return 1;
    return 0;
}

// 测试升序排序
void test_sort_students_ascending(void) {
    Student students[] = {{"Alice", 1, 85}, {"Bob", 2, 75}, {"Charlie", 3, 90}};
    int size = sizeof(students) / sizeof(students[0]);
    qsort(students, size, sizeof(Student), compare_students_by_score);
    CU_ASSERT_EQUAL(students[0].score, 75);
    CU_ASSERT_EQUAL(students[1].score, 85);
    CU_ASSERT_EQUAL(students[2].score, 90);
}

// 测试降序排序
void test_sort_students_descending(void) {
    Student students[] = {{"Alice", 1, 85}, {"Bob", 2, 75}, {"Charlie", 3, 90}};
    int size = sizeof(students) / sizeof(students[0]);
    qsort(students, size, sizeof(Student), compare_students_by_score_descending);
    CU_ASSERT_EQUAL(students[0].score, 90);
    CU_ASSERT_EQUAL(students[1].score, 85);
    CU_ASSERT_EQUAL(students[2].score, 75);
}

// 测试已有序数组(升序)
void test_sort_students_already_sorted_ascending(void) {
    Student students[] = {{"Bob", 2, 75}, {"Alice", 1, 85}, {"Charlie", 3, 90}};
    int size = sizeof(students) / sizeof(students[0]);
    qsort(students, size, sizeof(Student), compare_students_by_score);
    CU_ASSERT_EQUAL(students[0].score, 75);
    CU_ASSERT_EQUAL(students[1].score, 85);
    CU_ASSERT_EQUAL(students[2].score, 90);
}

// 测试含重复值数组
void test_sort_students_with_duplicate_scores(void) {
    Student students[] = {{"Alice", 1, 85}, {"Bob", 2, 85}, {"Charlie", 3, 90}};
    int size = sizeof(students) / sizeof(students[0]);
    qsort(students, size, sizeof(Student), compare_students_by_score);
    CU_ASSERT_EQUAL(students[0].score, 85);
    CU_ASSERT_EQUAL(students[1].score, 85);
    CU_ASSERT_EQUAL(students[2].score, 90);
}

在这段代码中,首先定义了Student结构体,包含姓名、学号和成绩三个成员。然后编写了两个比较函数compare_students_by_score和compare_students_by_score_descending,分别用于按成绩升序和降序排序。接着,针对升序排序、降序排序、已有序数组(升序)和含重复值数组等不同场景,编写了相应的测试函数。每个测试函数通过调用qsort函数对结构体数组进行排序,并使用CU_ASSERT_EQUAL断言来验证排序结果是否符合预期 。

3.2 复杂模块测试

3.2.1 学生成绩管理模块测试

"学生成绩管理" 模块是一个较为复杂的系统,涉及学生信息的添加、查询、修改、删除等操作。这里以add_student函数为例,测试正常添加、学号重复、成绩超出范围等情况:

c 复制代码
#include <CUnit/CUnit.h>
#include <CUnit/Basic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_STUDENTS 100

// 定义学生结构体
typedef struct {
    int id;
    char name[50];
    float score;
} Student;

Student students[MAX_STUDENTS];
int student_count = 0;

// 添加学生函数
int add_student(int id, const char *name, float score) {
    if (student_count >= MAX_STUDENTS) {
        return 0; // 学生已满
    }
    for (int i = 0; i < student_count; i++) {
        if (students[i].id == id) {
            return 0; // 学号重复
        }
    }
    if (score < 0 || score > 100) {
        return 0; // 成绩超出范围
    }
    students[student_count].id = id;
    strcpy(students[student_count].name, name);
    students[student_count].score = score;
    student_count++;
    return 1;
}

// 测试正常添加
void test_add_student_normal(void) {
    student_count = 0;
    int result = add_student(1, "Alice", 85);
    CU_ASSERT_EQUAL(result, 1);
    CU_ASSERT_EQUAL(students[0].id, 1);
    CU_ASSERT_STRING_EQUAL(students[0].name, "Alice");
    CU_ASSERT_EQUAL(students[0].score, 85);
}

// 测试学号重复
void test_add_student_duplicate_id(void) {
    student_count = 0;
    add_student(1, "Alice", 85);
    int result = add_student(1, "Bob", 90);
    CU_ASSERT_EQUAL(result, 0);
}

// 测试成绩超出范围
void test_add_student_invalid_score(void) {
    student_count = 0;
    int result = add_student(2, "Charlie", 105);
    CU_ASSERT_EQUAL(result, 0);
}

在上述代码中,add_student函数负责将学生信息添加到students数组中。test_add_student_normal函数测试正常添加学生的情况,验证函数返回值为 1 且学生信息被正确添加。test_add_student_duplicate_id函数测试学号重复的情况,验证函数返回值为 0 表示添加失败。test_add_student_invalid_score函数测试成绩超出范围的情况,同样验证函数返回值为 0 。

3.2.2 文件加密模块测试

"文件加密" 模块用于对文件进行加密和解密操作,确保文件内容的安全性。这里以encrypt_file函数为例,测试空文件、小文件、大文件,验证加密后解密能恢复原文件:

c 复制代码
#include <CUnit/CUnit.h>
#include <CUnit/Basic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 简单的异或加密函数
void encrypt_file(const char *input_file, const char *output_file, char key) {
    FILE *input = fopen(input_file, "rb");
    FILE *output = fopen(output_file, "wb");
    if (input == NULL || output == NULL) {
        perror("Failed to open file");
        exit(EXIT_FAILURE);
    }
    int ch;
    while ((ch = fgetc(input)) != EOF) {
        ch = ch ^ key;
        fputc(ch, output);
    }
    fclose(input);
    fclose(output);
}

// 测试空文件
void test_encrypt_empty_file(void) {
    const char *input_file = "empty.txt";
    const char *encrypted_file = "encrypted_empty.txt";
    const char *decrypted_file = "decrypted_empty.txt";
    char key = 'A';

    // 创建空文件
    FILE *fp = fopen(input_file, "wb");
    fclose(fp);

    encrypt_file(input_file, encrypted_file, key);
    encrypt_file(encrypted_file, decrypted_file, key);

    FILE *input = fopen(input_file, "rb");
    FILE *decrypted = fopen(decrypted_file, "rb");
    int ch1, ch2;
    while ((ch1 = fgetc(input)) != EOF && (ch2 = fgetc(decrypted)) != EOF) {
        CU_ASSERT_EQUAL(ch1, ch2);
    }
    fclose(input);
    fclose(decrypted);
}

// 测试小文件
void test_encrypt_small_file(void) {
    const char *input_file = "small.txt";
    const char *encrypted_file = "encrypted_small.txt";
    const char *decrypted_file = "decrypted_small.txt";
    char key = 'A';

    // 创建小文件并写入内容
    FILE *fp = fopen(input_file, "wb");
    fputs("Hello, World!", fp);
    fclose(fp);

    encrypt_file(input_file, encrypted_file, key);
    encrypt_file(encrypted_file, decrypted_file, key);

    FILE *input = fopen(input_file, "rb");
    FILE *decrypted = fopen(decrypted_file, "rb");
    int ch1, ch2;
    while ((ch1 = fgetc(input)) != EOF && (ch2 = fgetc(decrypted)) != EOF) {
        CU_ASSERT_EQUAL(ch1, ch2);
    }
    fclose(input);
    fclose(decrypted);
}

// 测试大文件(假设大文件为10MB)
void test_encrypt_large_file(void) {
    const char *input_file = "large.txt";
    const char *encrypted_file = "encrypted_large.txt";
    const char *decrypted_file = "decrypted_large.txt";
    char key = 'A';

    // 创建大文件(这里简单填充10MB数据)
    FILE *fp = fopen(input_file, "wb");
    for (int i = 0; i < 10 * 1024 * 1024; i++) {
        fputc('A', fp);
    }
    fclose(fp);

    encrypt_file(input_file, encrypted_file, key);
    encrypt_file(encrypted_file, decrypted_file, key);

    FILE *input = fopen(input_file, "rb");
    FILE *decrypted = fopen(decrypted_file, "rb");
    int ch1, ch2;
    while ((ch1 = fgetc(input)) != EOF && (ch2 = fgetc(decrypted)) != EOF) {
        CU_ASSERT_EQUAL(ch1, ch2);
    }
    fclose(input);
    fclose(decrypted);
}

在这段代码中,encrypt_file函数使用简单的异或加密算法对文件进行加密。针对空文件、小文件和大文件,分别编写了test_encrypt_empty_file、test_encrypt_small_file和test_encrypt_large_file测试函数。每个测试函数先创建相应的测试文件,然后进行加密和解密操作,最后通过比较原始文件和解密后文件的内容,使用CU_ASSERT_EQUAL断言来验证加密和解密的正确性。

3.3 测试报告分析

3.3.1 生成 HTML 报告

CUnit 提供了生成 HTML 格式测试报告的功能,通过配置CU_set_output_filename函数可以指定报告的文件名。以下是生成 HTML 报告的示例代码:

c 复制代码
#include <CUnit/CUnit.h>
#include <CUnit/Automated.h>

// 假设前面已经定义了各种测试函数和测试套件

int main() {
    CU_pSuite pSuite = NULL;

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

    // 添加测试套件
    pSuite = CU_add_suite("My Test Suite", NULL, NULL);
    if (NULL == pSuite) {
        CU_cleanup_registry();
        return CU_get_error();
    }

    // 添加测试函数到测试套件
    // 假设已经定义了test_binary_search_exist等测试函数
    if (NULL == CU_add_test(pSuite, "Test binary search exist", test_binary_search_exist) ||
        NULL == CU_add_test(pSuite, "Test binary search not exist", test_binary_search_not_exist) ||
        NULL == CU_add_test(pSuite, "Test add student normal", test_add_student_normal) ||
        // 其他测试函数的添加
        ) {
        CU_cleanup_registry();
        return CU_get_error();
    }

    // 设置输出报告文件名
    CU_set_output_filename("test_report");
    // 设置报告格式为HTML
    CU_list_tests_to_file(CU_HTML);

    // 运行所有测试
    CU_automated_run_tests();

    // 清理测试注册表
    CU_cleanup_registry();
    return CU_get_error();
}

在上述代码中,首先初始化 CUnit 测试注册表,添加测试套件和测试函数。然后通过CU_set_output_filename函数将输出报告文件名设置为test_report,并使用CU_list_tests_to_file(CU_HTML)指定生成 HTML 格式的报告。最后运行所有测试并清理测试注册表。生成的 HTML 报告中会详细列出每个测试用例的执行结果,包括测试用例名称、是否通过、失败时的详细错误信息等。通过查看报告中的测试通过率,可以直观地了解整个测试的执行情况。如果有测试用例失败,报告中会明确指出失败的测试用例名称和具体的错误信息,方便开发者快速定位和解决问题。例如,如果某个断言失败,报告中会显示预期值和实际值的差异,帮助开发者分析代码中存在的问题 。

3.3.2 持续集成思路

持续集成(CI)的核心目标是让单元测试 "自动化、常态化"------ 通过工具链将 "代码提交→编译构建→单元测试→结果反馈" 的流程串联起来,避免人工操作遗漏,同时确保每次代码变更都能被即时验证,从源头阻断 "带病代码" 流入后续环节。结合 CUnit 单元测试与 C 语言项目的特性,可按以下步骤落地:​

3.3.2.1 核心前提:将单元测试集成到 CMake 构建流程​

CMake 是 C/C++ 项目最常用的跨平台构建工具,将 CUnit 测试嵌入 CMake 流程,是实现自动化测试的基础。具体配置步骤如下:​

  1. 项目结构规划
    建议采用 "源码与测试代码分离" 的目录结构,例如:
c 复制代码
project_root/
├── src/          # 核心业务代码(如二分查找、学生成绩管理模块)
│   ├── search.c  # 二分查找函数实现
│   ├── score.c   # 学生成绩管理模块实现
│   └── CMakeLists.txt  # 源码构建配置
├── test/         # 单元测试代码目录
│   ├── test_search.c  # 二分查找的CUnit测试用例
│   ├── test_score.c   # 成绩管理模块的CUnit测试用例
│   └── CMakeLists.txt  # 测试代码构建配置
└── CMakeLists.txt      # 项目根目录CMake配置

  1. 配置测试代码的 CMakeLists.txt

在test/CMakeLists.txt中,需完成 "链接 CUnit 库、生成测试可执行文件、注册测试目标" 三大核心操作,示例配置如下:​

c 复制代码
# 1. 查找CUnit库(需确保系统已安装或指定库路径)
find_package(CUnit REQUIRED)
if(NOT CUNIT_FOUND)
    message(FATAL_ERROR "CUnit library not found! Please install libcunit-dev first.")
endif()

# 2. 生成测试可执行文件(将所有测试用例源码编译为一个测试程序)
add_executable(run_tests 
    test_search.c 
    test_score.c 
    ../src/search.c  # 依赖的核心业务代码(或链接业务代码生成的静态库)
    ../src/score.c
)

# 3. 链接CUnit库与业务代码依赖
target_link_libraries(run_tests 
    PRIVATE ${CUNIT_LIBRARIES}  # 链接CUnit库
)

# 4. 配置头文件路径(让测试代码能找到CUnit头文件和业务代码头文件)
target_include_directories(run_tests 
    PRIVATE ${CUNIT_INCLUDE_DIRS}
    PRIVATE ../src  # 业务代码头文件目录
)

# 5. 注册测试目标(关键步骤:让CMake识别"test"命令)
add_test(
    NAME AllUnitTests  # 测试目标名称(自定义)
    COMMAND run_tests  # 测试可执行文件路径
)

# 可选:设置测试失败时停止构建(严格模式,根据需求开启)
set_tests_properties(AllUnitTests PROPERTIES FAIL_REGULAR_EXPRESSION "FAILED")
​
  1. 验证 CMake 集成效果
    配置完成后,在项目根目录执行以下命令,即可自动编译并运行单元测试:
c 复制代码
# 创建构建目录(避免污染源码目录)
mkdir build && cd build
# 生成Makefile(或VS工程文件,取决于操作系统)
cmake ..
# 编译源码与测试代码
make
# 自动运行所有注册的单元测试
make test  # 或直接执行 ./test/run_tests​
​

若所有测试用例通过,会输出类似100% tests passed, 0 tests failed out of X的结果;若有失败用例,会明确标注失败的测试函数与断言位置,便于快速定位问题。​

3.3.2.2 进阶:对接常用 CI 平台实现 "提交即测试"​

将 CMake+CUnit 的测试流程接入 CI 平台(如 GitHub Actions、GitLab CI、Jenkins),可实现 "代码提交 / 合并请求时自动触发测试",无需人工干预。以下以最常用的GitHub Actions为例,说明配置方法:​

  1. 创建 CI 配置文件
    在项目根目录下创建.github/workflows/cunit-test.yml文件,内容如下(支持 Linux 和 Windows 双平台测试):
c 复制代码
name: CUnit Unit Tests

# 触发条件:代码提交到main分支、或创建合并请求到main分支时
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

# 定义要运行的任务(多平台并行测试)
jobs:
  test:
    name: Test on ${{ matrix.os }}
    runs-on: ${{ matrix.os }}  # 运行环境:Linux和Windows
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]

    steps:
      # 步骤1:拉取项目代码
      - name: Checkout code
        uses: actions/checkout@v4

      # 步骤2:安装依赖(Linux需安装CUnit,Windows用MinGW编译CUnit)
      - name: Install dependencies (Linux)
        if: matrix.os == 'ubuntu-latest'
        run: |
          sudo apt-get update
          sudo apt-get install -y libcunit1 libcunit1-dev cmake gcc  # 安装CUnit和构建工具

      - name: Install dependencies (Windows)
        if: matrix.os == 'windows-latest'
        uses: ilammy/msvc-dev-cmd@v1  # 配置Windows编译环境
        with:
          arch: x64
        # 可选:Windows下若未预安装CUnit,可通过源码编译(需提前准备编译脚本)
        run: |
          git clone https://git.code.sf.net/p/cunit/code CUnit  # 克隆CUnit源码
          cd CUnit
          mkdir build && cd build
          cmake .. -G "MinGW Makefiles" -DCMAKE_INSTALL_PREFIX=./install  # 生成MinGW构建文件
          mingw32-make install  # 编译并安装CUnit到指定目录
          echo "CUNIT_INSTALL_DIR=$(pwd)/install" >> $GITHUB_ENV  # 记录CUnit安装路径

      # 步骤3:编译项目并运行单元测试
      - name: Build and run tests
        run: |
          mkdir build && cd build
          # Windows需指定CUnit的安装路径(Linux可自动查找)
          if [ "${{ matrix.os }}" = "windows-latest" ]; then
            cmake .. -DCUNIT_INCLUDE_DIRS="${{ env.CUNIT_INSTALL_DIR }}/include" -DCUNIT_LIBRARIES="${{ env.CUNIT_INSTALL_DIR }}/lib/libcunit.a"
          else
            cmake ..
          fi
          cmake --build .  # 编译(等同于make或VS构建)
          ctest --output-on-failure  # 运行测试,失败时输出详细日志

  1. CI 流程的核心价值​
    • 即时反馈:开发者提交代码后,CI 平台会在几分钟内完成编译和测试,若有错误,会在 GitHub 的 "Actions" 页面或合并请求中高亮显示,开发者可立即修复;
    • 跨平台验证:通过配置多操作系统(Linux/Windows)、多编译器(GCC/Clang/MSVC),避免 "本地测试通过、其他环境失败" 的兼容性问题;
    • 强制测试门槛:可在 CI 配置中设置 "测试不通过则禁止合并代码",确保合并到主分支的代码一定是 "测试通过" 的,从流程上保障代码质量。
3.3.2.3 持续集成的维护要点​
  1. 测试用例的 "轻量化"​
    单元测试应聚焦 "最小功能单元",避免编写依赖外部资源(如数据库、网络接口)的重型测试用例 ------ 这类用例执行慢、稳定性差,会拖慢 CI 流程(建议将重型测试归为 "集成测试",单独触发)。
  2. 测试报告的归档​
    在 CI 流程中,可通过CU_set_output_filename生成 HTML 测试报告,并使用 CI 平台的 "artifacts" 功能归档报告(例如在 GitHub Actions 中添加actions/upload-artifact步骤),便于后续追溯历史测试结果。
  3. 定期清理 "冗余用例"​
    随着代码迭代,部分测试用例会因功能废弃而失效,需定期(如每季度)梳理测试用例,删除冗余或过时的用例,避免 CI 流程中出现 "无效测试" 占用资源。
相关推荐
Shylock_Mister2 小时前
弱函数:嵌入式回调的最佳实践
c语言·单片机·嵌入式硬件·物联网
攒钱植发2 小时前
嵌入式Linux——“大扳手”与“小螺丝”:为什么不该用信号量(Semaphore)去模拟“完成量”(Completion)
linux·服务器·c语言
三品吉他手会点灯3 小时前
STM32F103学习笔记-16-RCC(第3节)-使用HSE配置系统时钟并使用MCO输出监控系统时钟
c语言·笔记·stm32·单片机·嵌入式硬件·学习
jzhwolp3 小时前
从nginx角度看数据读写,阻塞和非阻塞
c语言·nginx·性能优化
oioihoii7 小时前
《C语言点滴》——笑着入门,扎实成长
c语言·开发语言
say_fall9 小时前
C语言容易忽略的小知识点(1)
c语言·开发语言
安冬的码畜日常9 小时前
【JUnit实战3_31】第十九章:基于 JUnit 5 + Hibernate + Spring 的数据库单元测试
spring·单元测试·jdbc·hibernate·orm·junit5
程序员东岸11 小时前
数据结构精讲:从栈的定义到链式实现,再到LeetCode实战
c语言·数据结构·leetcode
say_fall11 小时前
C语言容易被忽略的易错点(2)
c语言·开发语言