当前位置: 首页 > news >正文

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

目录

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

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,编译命令如下:

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:
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),可以定义如下测试函数:

#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函数添加到测试套件的代码示例:
#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 二分查找函数测试

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

#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函数对其按成绩进行排序,并编写相应的测试用例:

#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函数为例,测试正常添加、学号重复、成绩超出范围等情况:

#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函数为例,测试空文件、小文件、大文件,验证加密后解密能恢复原文件:

#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 报告的示例代码:

#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");// 设置报告格式为HTMLCU_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. 项目结构规划
    建议采用 “源码与测试代码分离” 的目录结构,例如:​
project_root/
├── src/          # 核心业务代码(如二分查找、学生成绩管理模块)
│   ├── search.c  # 二分查找函数实现
│   ├── score.c   # 学生成绩管理模块实现
│   └── CMakeLists.txt  # 源码构建配置
├── test/         # 单元测试代码目录
│   ├── test_search.c  # 二分查找的CUnit测试用例
│   ├── test_score.c   # 成绩管理模块的CUnit测试用例
│   └── CMakeLists.txt  # 测试代码构建配置
└── CMakeLists.txt      # 项目根目录CMake配置


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

# 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 集成效果
    配置完成后,在项目根目录执行以下命令,即可自动编译并运行单元测试:​
# 创建构建目录(避免污染源码目录)
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 双平台测试):​
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和Windowsstrategy:matrix:os: [ubuntu-latest, windows-latest]steps:# 步骤1:拉取项目代码- name: Checkout codeuses: actions/checkout@v4# 步骤2:安装依赖(Linux需安装CUnit,Windows用MinGW编译CUnit)- name: Install dependencies (Linux)if: matrix.os == 'ubuntu-latest'run: |sudo apt-get updatesudo 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 CUnitmkdir build && cd buildcmake .. -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 testsrun: |mkdir build && cd build# Windows需指定CUnit的安装路径(Linux可自动查找)if [ "${{ matrix.os }}" = "windows-latest" ]; thencmake .. -DCUNIT_INCLUDE_DIRS="${{ env.CUNIT_INSTALL_DIR }}/include" -DCUNIT_LIBRARIES="${{ env.CUNIT_INSTALL_DIR }}/lib/libcunit.a"elsecmake ..ficmake --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 流程中出现 “无效测试” 占用资源。
http://www.dtcms.com/a/588966.html

相关文章:

  • 会小二也是做会议网站的小地方做外卖网站怎样
  • python+playwright自动化如何解决文件上传问题
  • Linux介绍及常用命令
  • PyTorch中张量和模型的核心属性解析
  • 哈尔滨网站设计公司公司名字大全免费版
  • 大模型知识编辑技术——李宏毅2025《机器学习》第十讲
  • JAVA中next和nextLine的区别
  • 东莞设计网站企业淘宝客建站需要多少钱
  • ROS2 Humble 笔记(十二)launch 文件与 namespace 启动多个节点
  • nginx源码安装以及平滑升级
  • [特殊字符] Spring AOP 注解方式详解
  • C++——二叉搜索树
  • 青少年机器人技术等级考试理论综合试卷(一级)2020年9月
  • Redis_9_Set
  • 计算机网络培训课程大庆网站建设优化
  • 网站正在建设中永久wordpress 前台文章
  • Electron 桌面应用开发入门指南:从零开始打造 Hello World
  • 深入解析手机快充技术原理与实现
  • JavaScript 数组方法大全
  • 电子商务网站建设与管理的实验报告个人怎样免费建网站
  • STM32F103学习笔记-16-RCC(第3节)-使用HSE配置系统时钟并使用MCO输出监控系统时钟
  • LeRobot 入门教程(十五)从Hub加载环境
  • HTML DOM 总结
  • 社群经济下开源链动2+1模式AI智能名片S2B2C商城小程序的信任重构机制研究
  • Git 命令大全:从基础到高级操作
  • Git_Rebase
  • 【深度学习|学习笔记】异常检测概论 — 从经典算法到深度学习(含实用 Python 示例)
  • 如何建立一个视频网站html5 手机网站页面实例
  • FlutterPlugin接口实现与插件架构设计
  • 图漾GM461-E1相机专栏