cmocka单元测试框架
cmocka是一个C语言的单元测试框架,仅依赖标准库,可以在多种平台多种编译器上使用。
cmocka官网为https://cmocka.org/
下载和编译
cmocka的源码托管在GitLab上,编译系统使用CMake
1 | git clone https://gitlab.com/cmocka/cmocka.git |
示例
cmocka使用示例位于源码目录下的example文件夹中,example中演示了assert_macro、assert_module、allocate_module和mock的使用。
测试用法
simple_testsimple_test.c演示了cmocka最简单的使用方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// test case, do nothing
static void null_test_success(void **state) {
(void) state; /* unused */
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(null_test_success),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}allocate_module_test该例子演示了
allocate检测功能的使用, 对应的源码为example/allocate_module.c和example/allocate_module_test.c,其中allocate_module.c是待测试模块。根据源码分析,为了使用
memory check功能,需要修改待测模块的源码1
2
3
4
5
6
7
8
9
10
extern void* _test_malloc(const size_t size, const char* file, const int line);
extern void* _test_calloc(const size_t number_of_elements, const size_t size,
const char* file, const int line);
extern void _test_free(void* const ptr, const char* file, const int line);将代码中使用的
malloc、free等函数替换成cmocka框架中的封装,然后在test case中调用待测函数1
2
3
4static void leak_memory_test(void **state) {
(void) state; /* unused */
leak_memory();
}示例程序中演示了检测内存泄漏、缓冲区溢出和缓冲区下溢;内存问题的检测通过替换
malloc和free函数来完成,使用场景比较有限。assert_macro_test该示例演示了断言宏的使用,用法非常简单,在
test case中使用断言宏对待测试模块进行调用即可1
2
3
4
5
6
7
8
9
10
11static void get_status_code_string_test(void **state) {
(void) state; /* unused */
assert_string_equal(get_status_code_string(0), "Address not found");
assert_string_equal(get_status_code_string(1), "Connection timed out");
}
static void string_to_status_code_test(void **state) {
(void) state; /* unused */
assert_int_equal(string_to_status_code("Address not found"), 0);
assert_int_equal(string_to_status_code("Connection timed out"), 1);
}这些断言宏仅判断测试结果是否与预期相同。
assert_module_test在这个例子中演示了
assert相关宏的更高级的用法:mock_assert在
example/assert_module.c中,使用mock_assert宏覆盖了标准库中的assert宏,原因是标准库中的assert宏会引起进程的Aborted,造成无法继续执行其他test case,而mock_assert不会引起进程Aborted。expect_assert_failure根据语义可以判断这个宏的作用是期望断言失败,即使用该宏测试的函数中发生断言失败,则该宏测试通过,否则测试失败。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31// 待测试函数
void increment_value(int * const value) {
assert(value);
(*value) ++;
}
void decrement_value(int * const value) {
if (value) {
(*value) --;
}
}
// test case
static void increment_value_assert(void **state) {
(void) state;
expect_assert_failure(increment_value(NULL));
}
static void decrement_value_fail(void **state) {
(void) state;
expect_assert_failure(decrement_value(NULL));
}
// 测试结果
[ RUN ] increment_value_assert
Expected assertion value occurred
[ OK ] increment_value_assert
[ RUN ] decrement_value_fail
Expected assert in decrement_value(NULL)
[ ERROR ] --- [ LINE ] --- /home/noah/cmocka/example/assert_module_test.c:46: error: Failure!
[ FAILED ] decrement_value_fail
mock用法
mock功能的演示代码位于example/mock中,提供了两个示例,分别是chef_wrap和uptime。
mock功能的使用依赖于一个连接器参数:--wrap=symbol,如果在编译时使用,需要用-Wl,--wrap=symbol,使用这个参数后,当需要调用symbol函数时,实际上会去调用__wrap_symbol。
chef_wrap在这个例子中,待测模块是位于
waiter_test_wrap.c中的waiter_process函数,该函数中使用了chef_cook函数,但是根据chef.c中chef_cook函数的表述,该函数并未实现,所以需要对这个函数进行mock,__wrap_chef_cook便是该函数的mock。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24int __wrap_chef_cook(const char *order, char **dish_out)
{
bool has_ingredients;
bool knows_dish;
char *dish;
check_expected_ptr(order); // 测试输入是否为期望值
knows_dish = mock_type(bool); // mock knows_dish
if (knows_dish == false) {
return -1;
}
has_ingredients = mock_type(bool); // mock has_ingredients
if (has_ingredients == false) {
return -2;
}
dish = mock_ptr_type(char *); // mock dish
*dish_out = strdup(dish);
if (*dish_out == NULL) return ENOMEM;
return mock_type(int); // mock return value
}该
mock函数的实现中,有四处mock_type,这些变量的值由外部(如test case中)提供;check_expected_ptr宏用来测试变量是否为期望的值,该期望值也由外部指定。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23static void test_order_hotdog(void **state)
{
int rv;
char *dish;
(void) state; /* unused */
/* 指定check_expected_ptr的期望值 */
expect_string(__wrap_chef_cook, order, "hotdog");
will_return(__wrap_chef_cook, true); // mock knows_dish
will_return(__wrap_chef_cook, true); // mock has_ingredients
/* mock dish */
will_return(__wrap_chef_cook, cast_ptr_to_largest_integral_type("hotdog"));
will_return(__wrap_chef_cook, 0); // mock return value
rv = waiter_process("hotdog", &dish);
assert_int_equal(rv, 0);
assert_string_equal(dish, "hotdog");
if (dish != NULL) {
free(dish);
}
}uptime该示例中编译后生成两个可执行文件,分别是
uptime和test_uptime;其中uptime使用未被mock的uptime函数,而test_uptime使用mock的uptime函数。这个例子旨在演示
mock函数的用途,以及开发过程中mock在进行单元测试时的作用。
生成测试报告
cmocka生成的xml格式报告为JUnit格式。
一般情况下,执行cmocka单元测试程序,测试结果会直接打印到stderr上,格式如下
1 | [==========] Running 2 test(s). |
如果需要生成xml格式报告,需要在代码中添加如下行
1 | cmocka_set_message_output(CM_OUTPUT_XML); |
该行需要在cmocka_run_group_tests之前调用;xml格式输出如下
1 |
|
除此之外,也可以通过设置CMOCKA_MESSAGE_OUTPUT环境变量修改cmocka的输出格式,环境变量可用的值有stdout、subunit、tab和 xml;需要注意的是,设置环境变量修改报告格式的方法优先级更高。
默认情况下,cmocka的输出会被打印到stderr,如果需要存储到文件中,可以通过设置CMOCKA_XML_FILE环境变量的方式,如
1 | CMOCKA_XML_FILE=testresults/result1.xml |
如果cmocka无法在CMOCKA_XML_FILE指定的位置创建文件,则仍然会将结果输出到stderr。
如果有多个cmocka测试程序需要生成报告,可以使用%g对文件名进行格式化
1 | CMOCKA_XML_FILE=testresults/%g.xml |
生成报告时,%g将被格式化为group name,即cmocka_run_group_tests宏的第一个参数,例如前面simple_test生成的报告文件名为tests.xml。
生成覆盖率报告
推荐使用lcov工具生成代码覆盖率报告;lcov依赖于gcov,后者是包含在GNU编译套件中的,只要安装了GCC,一般就已经包含了gcov工具,但是lcov需要单独安装;Ubuntu上安装方法如下
1 | sudo apt install lcov |
为了生成代码覆盖率报告,首先需要在编译生成单元测试程序时,添加编译器和连接器参数
1 | # 编译器参数 |
编译完成后,可执行文件同级目录下应该会生成后缀名为.gcno的文件,如果使用cmake编译系统进行编译,生成的文件可能在对应项目的CMakeFiles目录中;继续执行测试程序,执行完成后,会在当前目录生成后缀名为.gcda的文件,如果使用cmake,则会生成在CMakeFiles对应的目录中。
接着使用lcov分析并生成对应的info文件
1 | lcov --capture --directory project-dir --output-file coverage.info |
注意将project-dir替换成包含.gcda文件的文件夹(支持递归查找)
最后使用genhtml工具将前面生成的info文件转为html格式的网页
1 | genhtml coverage.info --output-directory out |
生成的静态网页会存放在out文件夹中,使用浏览器打开index.html即可可视化查看代码覆盖率

