基于CMocka的单元测试编写与架构方案咨询
基于CMocka的测试组织方案
针对你的场景,为每个产品代码函数单独创建测试文件并编译为独立可执行文件不仅是可行的,更是推荐的最佳实践——这正好解决了你提到的「不同测试需要不同Mock逻辑」「避免Mock冲突」的核心问题。下面我会详细拆解具体的实现思路和技巧:
1. 测试文件的组织逻辑
把每个被测函数对应到一个独立的测试文件:
- 比如产品代码
a.c里的函数fetch_data(),对应测试文件test_fetch_data.c;函数parse_json()对应test_parse_json.c - 每个测试文件只聚焦单个被测函数的所有测试用例(正常流程、错误分支、边界情况等),这样每个文件里的Mock函数可以完全贴合当前函数的需求,不会和其他测试的Mock逻辑冲突
2. 处理外部库(libcurl/libjson-c)的Mock
利用CMocka的__wrap_机制,每个测试文件只Mock当前被测函数实际调用到的外部库函数,不用一次性全量Mock:
- 比如
test_fetch_data.c里只需要Mockcurl_easy_init()、curl_easy_setopt()、curl_easy_perform()这些fetch_data()用到的curl函数,不用管json-c的函数 - 编译时通过
-Wl,--wrap=函数名告诉链接器替换原函数为你的__wrap_版本,每个测试可执行文件的链接参数可以单独配置(比如不同测试需要wrap的函数不一样)
3. 处理内部函数的Mock
对于被测函数调用的其他内部函数(比如fetch_data()调用了a.c里的log_error()),同样用__wrap_机制:
- 在对应的测试文件里实现
__wrap_log_error(),编写符合当前测试用例的逻辑(比如在错误分支测试里让它返回失败,或者记录调用参数) - 因为每个测试文件是独立编译的,不同测试文件里的
__wrap_log_error()可以有完全不同的实现,互不干扰
注意:如果内部函数是
static修饰的,__wrap_机制无法直接生效。这时候建议重构代码:要么去掉static(如果合理),要么把需要Mock的内部函数抽离到单独的模块文件中,这样更容易测试。
4. 编译与构建配置(以CMake为例)
用CMake可以很方便地批量管理这些独立的测试可执行文件,示例配置如下:
# 先编译产品代码为共享库 add_library(abc SHARED a.c) target_link_libraries(abc PRIVATE curl json-c) # 收集所有测试文件(命名规则:test_*.c) file(GLOB TEST_SOURCES test_*.c) foreach(test_source ${TEST_SOURCES}) # 提取测试可执行文件的名称(比如test_fetch_data.c -> test_fetch_data) get_filename_component(test_name ${test_source} NAME_WE) # 创建独立的测试可执行文件 add_executable(${test_name} ${test_source}) # 链接CMocka和产品库 target_link_libraries(${test_name} PRIVATE cmocka abc) # 配置当前测试需要wrap的函数(根据实际需求添加) target_link_options(${test_name} PRIVATE -Wl,--wrap=curl_easy_init -Wl,--wrap=curl_easy_perform -Wl,--wrap=log_error # 其他需要wrap的函数... ) # 将测试添加到CTest,方便批量运行 add_test(NAME ${test_name} COMMAND ${test_name}) endforeach()
5. 单个测试文件的编写示例
以test_fetch_data.c为例,展示如何编写带Mock的测试用例:
#include <stdarg.h> #include <stddef.h> #include <setjmp.h> #include <cmocka.h> #include "a.h" // 产品代码的头文件 // Mock curl_easy_init函数 static void *__wrap_curl_easy_init(void) { // 返回预设的mock值,可通过will_return设置 return mock(); } // Mock curl_easy_perform函数 static CURLcode __wrap_curl_easy_perform(void *curl) { // 检查传入的curl指针是否符合预期 check_expected_ptr(curl); // 返回预设的错误码或成功状态 return mock_type(CURLcode); } // Mock内部函数log_error static void __wrap_log_error(const char *fmt, ...) { // 检查日志格式字符串是否符合预期 check_expected(fmt); // 可以用va_args获取可变参数做断言,这里简化处理 va_list args; va_start(args, fmt); va_end(args); } // 测试正常流程:curl调用成功 static void test_fetch_data_success(void **state) { (void)state; // 未使用的参数,避免编译警告 // 设置Mock的预期行为 will_return(__wrap_curl_easy_init, (void*)0x123456); // 返回模拟的curl句柄 expect_value(__wrap_curl_easy_perform, curl, (void*)0x123456); will_return(__wrap_curl_easy_perform, CURLE_OK); expect_string(__wrap_log_error, fmt, "Fetch data success"); // 调用被测函数 int result = fetch_data("https://example.com"); // 断言结果正确 assert_int_equal(result, 0); } // 测试错误流程:curl初始化失败 static void test_fetch_data_init_failure(void **state) { (void)state; will_return(__wrap_curl_easy_init, NULL); expect_string(__wrap_log_error, fmt, "Curl init failed"); int result = fetch_data("https://example.com"); assert_int_equal(result, -1); } // 运行测试用例 int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_fetch_data_success), cmocka_unit_test(test_fetch_data_init_failure), }; return cmocka_run_group_tests(tests, NULL, NULL); }
6. 额外建议
- 如果多个测试文件需要共享一些通用的Mock逻辑(比如通用的curl错误处理Mock),可以把这些逻辑抽离到单独的头文件/源文件中,在测试文件里引入即可
- 尽量保持每个测试用例的独立性,一个用例只测试一个场景,这样出问题时更容易定位
内容的提问来源于stack exchange,提问作者upInCloud
相关产品推荐
相关产品推荐

