CMocka 是一针对 C 语言的单元测试框架,而且支持 mock 测试,其前身是由 Google 开发的 Cmockery,而后者由于不再维护,后来就通过 CMocka 继续开发维护。
也就因此,可以看到有些细节与同样由 Google 开发的 gmock 和 gtest 有些相似,这里详细介绍其使用方式。
简介
CMocka 实现很简单,除了头文件外,仅包含了一个源码文件,在使用时,需要先将 CMocka 编译成库,供测试程序链接使用,详细可以查看官网 cmocka.org 中的文档介绍。
该测试框架支持的特性包括了:
- 支持 Mock 对象,可设置模拟函数的期望返回值,期望输出参数,可检查模拟函数的输入参数、函数调用顺序等。
- 支持 Test Fixtures ,包括了 Setup 和 Teardown 。
- 不依赖第三方库,实际源码仅包含一个文件。
- 支持跨平台、多个编译器,例如常见 Linux、BSD、Windows 等,以及 GCC、LLVM、MSVC、MinGW 等。
源码安装
可以从 cmocka.org/files 上下载。
----- 解压
$ tar -xvf cmocka-1.1.3.tar.xz
----- 编译安装
$ cd cmocka-1.1.3 && mkdir build && cd build
基本示例
/* A test case that does nothing and succeeds. */
static void null_test_success(void **state)
{
(void) state; /* unused */
}
int main(void)
{
const struct unit_test tests[] = {
mockx_unit_test(null_test_success, /* setup */ NULL, /* teardown */ NULL),
};
return cmocka_run_group_tests(tests, /* group setup */ NULL, /* group teardown */ NULL);
}
在 main()
中定义了一个 struct unit_test
类型的数组,包含了多个测试用例,然后通过 cmocka_run_group_tests()
函数启动测试用例。无论是多个测试用例,还是单个,都可以通过 setup()
或者 teardown()
来初始化或者清理某些资源。
如下介绍常用的 API 及其使用方式。
断言
也就是直接判断某个条件,类似于标准库中的 assert()
语句,只是使用场景更加丰富。
// 布尔类型判断
void assert_true(expr);
void assert_false(expr);
// 指针判断
void assert_null(void *pointer);
void assert_non_null(void *pointer);
void assert_ptr_equal(void *a, void *b);
void assert_ptr_not_equal(void *a, void *b);
// 数值判断
void assert_int_equal(int a, int b);
void assert_int_not_equal(int a, int b);
void assert_float_equal(float a, float b, float epsilon);
void assert_float_not_equal(float a, float b, float epsilon);
// 字符串判断
void assert_string_equal(const char *a, const char *b);
void assert_string_not_equal(const char *a, const char *b);
void assert_memory_equal(const void *a, const void *b, size_t size);
void assert_memory_not_equal(const void *a, const void *b, size_t size);
// 范围判断
void assert_in_range(uintmax_t value, uintmax_t minimum, uintmax_t maximum);
void assert_not_in_range(uintmax_t value, uintmax_t minimum, uintmax_t maximum);
// Set判断
void assert_in_set(uintmax_t value, uintmax_t values[], size_t count);
void assert_not_in_set(uintmax_t value, uintmax_t values[], size_t count);
void assert_return_code(int rc, int error);
内存检测
可以用于测试内存溢出、访问越界、内存泄漏等问题,会跟踪所有使用 test_XXX()
接口分配的内存,在使用 test_free()
释放内存时,会检查内存块是否有内存溢出,结束时会检查是否有内存没有释放。
所需要做的就是将代码中的 malloc()
realloc()
等函数替换成 test_malloc()
test_realloc()
等函数。
#include <stdlib.h>
#include <setjmp.h>
#include <stdarg.h>
#include "cmocka.h"
#ifdef __TEST__
#define malloc(size) _test_malloc(size, __FILE__, __LINE__)
#define calloc(num, size) _test_calloc(num, size, __FILE__, __LINE__)
#define free(ptr) _test_free(ptr, __FILE__, __LINE__)
#endif
void leak_memory(void **state) {
(void) state;
int * const temporary = (int*)malloc(sizeof(int));
*temporary = 0;
}
void buffer_overflow(void **state) {
(void) state;
char * const memory = (char*)malloc(sizeof(int));
memory[sizeof(int)] = '!';
free(memory);
}
void buffer_underflow(void **state) {
(void) state;
char * const memory = (char*)malloc(sizeof(int));
memory[-1] = '!';
free(memory);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(leak_memory),
cmocka_unit_test(buffer_overflow),
cmocka_unit_test(buffer_underflow),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
注意,如果将上述的宏定义包含在头文件中,那么 stdlib.h
头文件需要在对应头文件之前,否则,因为上述定义可能会出现 expected declaration specifiers or '...' before string constant
的报错,最好使用如下定义。
#include <stdlib.h>
#include "cmocka.h"
#ifdef __TEST__
#define malloc(size) _test_malloc(size, __FILE__, __LINE__)
#define calloc(num, size) _test_calloc(num, size, __FILE__, __LINE__)
#define free(ptr) _test_free(ptr, __FILE__, __LINE__)
#endif
如下也介绍了与内存相关的内容,不过,不太建议使用该方案,感觉有很多的问题,不如使用比较成熟的方案,例如 valgrid 。
Mock
可以为 mock 函数设置返回值、输出参数、检查输入参数、检查调用顺序等,这也是最为复杂的部分了。
设置返回值
通过 will_return()
以及 mock()
的组合完成,其中 will_return()
设置某个函数期望返回的值,而 mock()
则返回通过 will_return()
设置的数据。
#include <stdlib.h>
#include <setjmp.h>
#include <stdarg.h>
#include "cmocka.h"
int return_integer(void)
{
return (int)mock();
}
int simple_call_integer(void)
{
return return_integer();
}
void simple_test(void **state) {
(void) state;
will_return(return_integer, 42);
assert_int_equal(simple_call_integer(), 42);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(simple_test),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
简单来说,通过 will_return()
将需要 mock 的函数及其返回值添加到一个链表中,然后,在调用 mock()
函数时将所需的返回值取出。
所以,这里的关键是如何保证需要模拟的函数可以覆盖原实现,尤其是动态库以及静态库,可能需要一些编译链接相关的技巧才可以。
配置 will_return()
的关键是 will_return_count()
函数,其它函数都是对该函数的封装,通过 count
参数配置不同的调用次数,包括了:A) N > 0
允许调用 N
次;B) -1
允许调用多次 (必须调用过);C) -2
可以调用多次或者不掉用。
也就对应了如下的几个宏定义。
will_return(function, value) // 只调用一次
will_return_count(function, value, count) // 指定调用次数,最终调用的函数
will_return_always(function, value) // 调用多次
will_return_maybe(function, value) // 调用多次或者不掉用
其它
跳过用例
有时候在编写测试用例时,为了方便测试,可以选定只执行或者过滤某些用例,只需要在执行 cmocka_run_group_tests()
函数之前,进行设置即可,可用接口如下。
void cmocka_set_test_filter(const char *pattern);
void cmocka_set_skip_filter(const char *pattern);
分别用来设置以及跳过用例,可以通过 *
和 ?
通配符进行设置。
内存申请
上述介绍的内存使用已经覆盖了绝大部分场景,不过还有与字符串相关的函数没有覆盖,例如 strdup()
strndup()
几个函数,此时需要使用上述类似的机制重新配置下。
#include <stdlib.h>
#include <setjmp.h>
#include <string.h>
#include <stdarg.h>
#include "cmocka.h"
char *_test_strdup(const char *ptr, const char *file, const int line)
{
char *ret;
size_t size = strlen(ptr);
ret = _test_malloc(size + 1, file, line);
memcpy(ret, ptr, size);
ret[size] = 0;
return ret;
}
char *_test_strndup(const char *ptr, const size_t size, const char *file, const int line)
{
char *ret;
ret = _test_malloc(size + 1, file, line);
memcpy(ret, ptr, size);
ret[size] = 0;
return ret;
}
与上类似,同样需要定义宏。
#ifdef __TEST__
extern char *_test_strdup(const char *ptr, const char *file, const int line);
extern char *_test_strndup(const char *ptr, const size_t size,
const char *file, const int line);
#define strdup(ptr) _test_strdup(ptr, __FILE__, __LINE__)
#define strndup(ptr, size) _test_strndup(ptr, size, __FILE__, __LINE__)
#endif
参考
- 一些常见的使用方式可以参考 cmocka 。