CMocka 使用简介

2017-12-06 language c/cpp

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