在自动化验证技术成熟之前,我们依旧需要测试,能否编写优秀的模块,体现的是能力,而为代码编写完善的测试用例,体现的则是习惯。
虽然测试并不能说明什么问题,但目前我们并无任何备选方案,相信在很长一段时间内,完善的测试用例对于项目而言都是弥足珍贵的。
有些时候会觉得编写测试用例太烦,但是要知道,至少我们还能通过测试用例来进行测试,有很多领域是无法通过如此“傻瓜式”的测试来达成目的的。
当然要是代码简单到用编译检测就能够排除bug,那测试就没什么用处了。但问题是哪个稍微复杂一点的程序能够做到?
为什么要写本文呢?不得不提到我目前对程序开发的理念:
写烂代码->维护代码->推翻重写
在日常开发过程中,我经常会因为各种原因重写一些模块或是进行一些重构,而我很难长时间集中注意力,且短时间内code review可能无法快读发现问题,这个时候,遗留的测试用例就能够在一定程度上帮助我。所以,完善的测试用例是一笔宝贵的财富。
本文主要介绍C语言的一款单元测试框架——cmocka,相比于junit之类的测试框架而言,会显得稍微繁琐和奇特,这主要是因为C的抽象层次不够高造成的,框架只能依托大量宏来解决问题,因此不可避免的需要用户处理一些额外的步骤。
0x00 cmocka的特性
如上是一张来自cmocka官网的图,使用cmocka编写的代码大体就是这样,其实还是能够接受的。
cmocka支持如下特性:
- 提供跨平台支持
- 文档完整详细
- 无第三方库依赖
- 非fork()执行
- 提供对于信号的处理
- 提供基本的抽象,如Test Fixture等,支持setup和teardown函数,集成mock
- 支持多种格式输出(编译时指定)
- 提供基本的内存检测,如内存泄露,buffer的上下溢检测
对于C程序而言,很多地方涉及到信号和系统调用,这个时候想要进行充分的测试,就需要强大的mock和信号处理的支持了,就上述问题而言,cmocka还是能够满足要求的,当然也有些许遗憾,多格式输出需要在编译cmocka时通过参数的方式指定。
0x01 cmocka的组成
上图是官方文档中提供的模块图,我们可以将其与cmocka提供的功能进行一些简单的映射。
- Running Tests 是整个测试的运行函数
- Standard Assertions 对标准assert的一种替换方案,防止测试被assert打断
- Checking Parameters 用于对函数参数的检测
- Mock Objects 用于mock对象,常用作mock函数的返回,实现方式与Checking Parameter类似
- Dynamic Memory Allocation 用于对内存的检测
- Assert Macros 包含了常用的断言
下文大致会覆盖上述内容,其中Standard Assertions和Dynamic Memory Allocation将不做阐述,对于后者,个人建议使用valgrind。
0x02 执行框架
本节我们通过官方的例子,来介绍测试代码的骨架,细节部分暂不提及,上代码:
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
/* A test case that does nothing and succeeds. */
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);
}
如果各位在之前接触过unittest,应该不难猜到这里的null_test_success函数就是具体的test case了,当然这里的test case内部并没有什么内容。
每个测试代码的开头都需要include 3个头文件,这是官方文档中明确要求的,它们分别是:
- stdarg.h
- stddef.h
- setjmp.h
在main函数中,可以看到一个类型为CMUnitTest的结构数组,每个CMUnitTest结构体都可以包含若干test case,以及可选的startup和teardown,startup和teardown就是test fixure,它们可以被若干test case共享,负责初始化或者销毁资源的操作。
cmocka_unit_test是一个宏,如果不深究的话,完全可以把它当做函数。除此之外,还有另外3个宏:
- cmocka_unit_test_startup(case, startup)
- cmocka_unit_test_teardown(case, teardown)
- cmocka_unit_test_startup_teardown(case, startup, teardown)
这个应该就不需要我解释了,名字已经表示得很清楚了。
cmocka_run_group_tests是一个函数,用来指示框架运行测试,可以为测试集指定全局的startup(参数2)和teardown(参数3)。此外,还有一个类似的函数,cmocka_run_group_tests_name它和前者基本相同,但是它可以通过第一个参数指定测试集的名字,其余3个参数和前者一致。
上述大致就是Running Tests模块的全部内容了,这里并未给出已经废弃的函数/宏,另外,还有3个比较使用的函数:
- void fail(void) 立即结束当前test case,报错
- void fail(const char* msg) 立即结束当前test case,报错并打印错误信息
- void skip(void) 跳过当前test case
请勿在test fixure中使用上述3个函数。
0x03 常用断言
我们没法在cmocka测试框架中使用assert.h中的断言,因为这会导致框架无法捕获错误,直接段错误。
为此cmocka框架为C语言中的常用类型提供了相对完善的断言函数,如下对常用的断言做简要阐述:
1. 表达式
- assert_true(expr)
- assert_false(expr)
其中expr表示C语言的表达式,相对简单,assert_true()和assert()的用法差不多,而assert_false()则是正好相反。
2. 整型
- assert_int_equal(int a, int b)
- assert_int_not_equal(int a, int b)
断言int型变量a与b是否相等。
- assert_in_range(LargestIntegralType v, LargestIntegralType min, LargestIntegralType max)
- assert_not_in_range(LargestIntegralType v, LargestIntegralType min, LargestIntegralType max)
断言v是否在[min, max]的范围之内。
- assert_in_set(LargestIntegralType v, LargestIntegralType vals[], size_t count)
- assert_not_in_set(LargestIntegralType v, LargestIntegralType vals[], size_t count)
断言v是否属于vals集合。
3. 指针
- assert_ptr_equal(void a, void b)
- assert_ptr_not_equal(void a, void b)
- assert_null(void* p)
- assert_non_null(void* p)
判断两个指针变量a与b是否相等,或者p指针是否为NULL。
4. 内存块
- assert_memory_equal(const void a, const void b, size_t size)
- assert_memory_not_equal(const void a, const void b, size_t size)
断言长度为size的a与b指向的内存内容是否相等。
5. string类型
C语言并没有字符串,只有char*,虽然我们也能使用内存块断言,但cmocka还是提供了字符串的断言。
- assert_string_equal(const char a, const char b)
- assert_string_not_equal(const char a, const char b)
断言a与b指向的字符串是否相等,而此处的size相当于strlen(a)或strlen(b)。
6. 函数返回值
- assert_return_code(int rc, int error)
断言函数的返回值rc是否小于0,并将errno传入,如果出错则将错误信息加入到输出中。感觉这个断言比较鸡肋。
0x04 mock对象
Java程序员应该或多或少都用过mockit,它和junit一起用相当方便,cmocka的mock object也提供类似功能,只不过实现的方式稍有不同罢了。
所谓mock,在C里就是仿造一个函数,可能有人会问,为何要mock,而不是使用真是的函数?原因在于,在很多情况下,我们无法或者是不需要对目标函数进行测试,这个函数可能是系统调用,也可能属于某个函数库,那么这个是否,无法也不应该对其进行测试。我们要做的只是伪造一个函数,即可完成我们需要的效果。
关于一个函数,在不考虑副作用的前提下,我们只需要关注它的输入(参数)和输出(返回值)即可。至于副作用,暂时没有比较好的解决办法。类似的,在程序验证中,也会对副作用进行一定的假设,或者说是一定的约束,但又无法杜绝,毕竟C语言不是函数式语言。
如下,我们按照对参数和对返回值的处理事件进行阐述,这里的事件和前文的断言使用方式有点不同,事件是先指定未来会出现什么事件,然后在未来某个时间点触发事件。
可以看做是如下的方式:
指定参数事件
指定返回值事件
rc func(para) {
触发参数事件
return 触发返回值事件
}
1. 参数断言
- check_expected(#para)
- check_expected_ptr(#para)
检查函数,但并不指出应该检查什么,这些都已经在函数调用之前通过如下的expect_*函数指定了。其中后者用于对指针类型的检查事件。
- expect_any(#func, #para)
- expect_any_count(#func, #para, size_t count)
指定func的para将会被传递的事件,如果有count,就说该事件会被重复count次。
- expect_check(#func, #para, #check_func, const void* check_data)
指定某个para将会触发调用check_func函数的事件,该函数的两个参数分别为para和check_data,若检查通过,则返回0。这个在文档中写的不是很明确,我通过测试得出的上述结论。
- expect_in_range(#func, #para, LargestIntegralType min, LargestIntegralType max)
- expect_not_in_range(#func, #para, LargestIntegralType min, LargestIntegralType max)
- expect_in_range_count(#func, #para, LargestIntegralType min, LargestIntegralType max, size_t count)
- expect_not_in_range_count(#func, #para, LargestIntegralType min, LargestIntegralType max, size_t count)
- expect_in_set(#func, #para, LargestIntegralType vals[])
- expect_not_in_set(#func, #para, LargestIntegralType vals[])
- expect_in_set_count(#func, #para, LargestIntegralType vals[], size_t count)
- expect_not_in_set_count(#func, #para, LargestIntegralType vals[], size_t count)
- expect_memory(#func, #para, void* mem, size_t size)
- expect_not_memory(#func, #para, void* mem, size_t size)
- expect_memory_count(#func, #para, void* mem, size_t size, size_t count)
- expect_not_memory_count(#func, #para, void* mem, size_t size, size_t count)
- expect_string(#func, #para, const char* str)
- expect_not_string(#func, #para, const char* str)
- expect_string_count(#func, #para, const char* str, size_t count)
- expect_not_string_count(#func, #para, const char* str, size_t count)
- expect_value(#func, #para, LargestIntegralType val)
- expect_not_value(#func, #para, LargestIntegralType val)
- expect_value_count(#func, #para, LargestIntegralType val, size_t count)
- expect_not_value_count(#func, #para, LargestIntegralType val, size_t count)
上述的这些expect函数基本和assert中的一致,其中expect_value和assert_int_equal类似,这里不再赘述。
2. 返回值断言
- mock()
- mock_ptr_type()
出发返回值事件,其中后者返回指针。
- will_return(#func, LargestIntegralType val)
- will_return_always(#func, LargestIntegralType val)
- will_return_count(#func, LargestIntegralType val, int count)
指定函数的返回事件,可以选择重复count次或者一直返回。
0x05 总结
如上基本就是cmocka测试框架的全部内容了,函数虽多,相信用过几次就会习惯了。
对于一般的程序测试,cmocka框架基本够用了,但是它还无法完全应对C语言的灵活性带来的复杂度,因此还是存在些许缺点的。
比如可能会被signal中断的函数就无法使用cmocka来测,输出文件类型需要在编译时指定等。
如果有需要,未来我可能会修改该测试框架,届时再做总结。