一:概述
CUnit是一个c语言的单元测试框架,它是以静态链接库的形式,连接到用户代码中的,主要的功能就是提供了语义丰富的断言和多种测试结果输出接口,可以方便地生成测试报告。
但是需要注意的地方是,由于Cunit和我们的代码是在同一个项目中,所以,需要注意将测试代码和程序代码进行区分管理,避免直接在程序代码中添加测试代码;为了达到这个目的,我们经常需要提供单独的头文件,在这个头文件中,可以将原有接口函数罗列进来,还可以将需要测试的内部使用的函数列入,这样,在测试用的.c文件中,就可以直接引用该头文件进行编译,连接,测试。
一个Registry可以认为是一个需要测试的独立的功能集合单元,我们可以使该单元处于active状态,以便进行运行测试,也可以使之处于inactive状态,这样,在一个运行中,我们可以指定要运行的Registry。
一个Suite可以若干运行条件类似的测试的集合,他需要注册到某一个Registry才可以被运行到。划分Suite的标准是多样的,例如,某些test运行之前需要进行特定的初始化动作,那么我们可以把凡是需要该类初始化动作的test放入到一个Suite中,因为以Suite为单位,可以有自己的初期化函数和清理函数。
一个test就可以认为是一个单元测试的函数了,由于Cunit是一个黑盒测试工具,也就是说,他的主要目的是根据输入参数和返回结果来从外部观察函数执行的是否正确,所以,通常的做法就是我们提供多种输入,然后使用Cunit提供的断言,来判断返回值,out形参数,和函数可能影响的全局变量的变化是否符合我们的设计。
Cunit提供的多种编程接口,通常是针对不同类型的程序需求。
Automated |
Output to xml file |
Non-interactive |
Basic |
Flexible programming interface |
Non-interactive |
Console |
Console interface (ansi C) |
Interactive |
Curses |
Graphical interface (Unix) |
Interactive |
如上边所示,后两种主要是interactive的接口,就是我们可以交互式地指定参数,然后观察结果。在我们具体的环境下,我们通常使用上面两种接口,第一种是将结果输出到XML文档中,便于我们生成报告。第二种仅仅是每一次运行结束之后,在standard output中显示,不能保留测试结果数据。
我们可以将前两种输出结合起来,自己测试的时候,使用Basic模式,生成报告的时候,使用Automated模式。
下面介绍一下典型的测试程序的书写流程:
1) 首先针对被测试的函数书写测试函数。
2) 初始化一个Registry。
3) 将特定的Suite加入到这个Registry中。可以为Suite指定初始化函数和清理函数。
4) 将测试函数加入到一个Suite中,这样,如果该测试函数的运行需要一些初始化条件,那么可以可以将代码加入到Suite的初始化函数中,当然不要忘记最后还要做对应的清理操作。
5) 使用相应的接口将测试结果输出。
6) 最后,清理Registry。
下面的程序是一个典型的例子:程序段1如下
这个例子中,有两个Suite,同时自己定义了一个函数 来将test加入到Suite中,这样的目的是避免main函数太长。另外这个函数中同时使用了Automated接口和Basic接口。我们不必同时使用,可以根据需要的形式进行选择。需要注意的是,使用Automated接口是,需要调用CU_list_tests_to_file()函数。
CUnit提供了大量的预定义的断言,针对几乎所有的C的标准类型,我们可以针对具体的需要检查的参数的类型进行选择。
提供的断言有:
CU_ASSERT(int
expression) |
Assert that expression is TRUE (non-zero) |
CU_ASSERT_TRUE(value) |
Assert that value is TRUE (non-zero) |
CU_ASSERT_FALSE(value) |
Assert that value is FALSE (zero) |
CU_ASSERT_EQUAL(actual,
expected) |
Assert that actual = = expected |
CU_ASSERT_NOT_EQUAL(actual,
expected)) |
Assert that actual != expected |
CU_ASSERT_PTR_EQUAL(actual,
expected) |
Assert that pointers actual = = expected |
CU_ASSERT_PTR_NOT_EQUAL(actual,
expected) |
Assert that pointers actual != expected |
CU_ASSERT_PTR_NULL(value) |
Assert that pointer value == NULL |
CU_ASSERT_PTR_NOT_NULL(value) |
Assert that pointer value != NULL |
CU_ASSERT_STRING_EQUAL(actual,
expected) |
Assert that strings actual and expected are equivalent |
CU_ASSERT_STRING_NOT_EQUAL(actual,
expected) |
Assert that strings actual and expected differ |
CU_ASSERT_NSTRING_EQUAL(actual,
expected, count) |
Assert that 1st count chars of actual andexpected are the same |
CU_ASSERT_NSTRING_NOT_EQUAL(actual,
expected, count) |
Assert that 1st count chars of actual andexpected differ |
CU_ASSERT_DOUBLE_EQUAL(actual,
expected, granularity) |
Assert
that |actual - expected|
<= |granularity| |
CU_ASSERT_DOUBLE_NOT_EQUAL(actual,
expected, granularity) |
Assert
that |actual - expected|
> |granularity| |
CU_PASS(message) |
Register a passing assertion with the specified message. No logical test is performed. |
CU_FAIL(message) |
Register a failed assertion with the specified message. No logical test is performed. |
可以看到,每一种类型的断言都有FATAL和非FATAL两个函数,他们的区别是,非FATAL断言失败的时候,程序继续运行,而FATAL类型的断言失败之后,程序马上中止。通常我们使用非FATAL就可以了。
注意在我们的程序中,需要对返回值,out参数进行判断,对于是否是预想结果的判断的时候,一定要使用断言,而不要使用printf等等函数,一方面printf缺乏灵活性,另一方面,只有使用断言,结果报告中才会有对应的输出项。
CU_PASS,CU_FAIL这两个断言比较特殊,它们仅仅表示测试程序运行到了这个地方。比如,在某些测试函数中,被测试的函数没有任何返回值等,我们为了证明这个函数已经被运行到了,我们使用以上两个函数。它们仅仅打印出一条消息,代表执行过。还有就是,它们也被输出。
下面程序段2给出了一个被测函数和一个测试函数,注意断言的使用:
测试用例是否完备,需要测试人员自己保证。
测试程序书写完成之后,我们需要将它们添加到某一个Suite中,在程序段1中我们看到了如何将一个Suite添加到Registry中,下面我们看看Suite的初始化和清理函数,以及如何将tests添加到Suite中。以下是Suite的初始化和清理程序段3:
这两个函数是在将Suite添加到Registry的时候指定的。参见程序段1。CU_add_suite函数的参数。
下面程序段4显示如何将tests加入Suite,以及测试函数本身
其中,testWRITE_GROUP是一个函数名。函数如下:
其中,write_group就是被测函数。
完成了以上步骤之后,剩下的就是生成测试报告了:以下是Basic模式下的典型的输出:
表中显示了一共有多少个Suite,多少个tests,还有一共执行了多少个断言.(当然,以上这么多断言是因为有循环。)
如果有失败的断言,那么会是如下的样子,在程序中,我们将write_group进行调整,让断言失败。
以上可以看出,报告会指出断言失败的tests数目,还可以指出,断言失败的位置和函数。
关于Automated模式的,如下所示:表一是测试结果的报告,显示该XML文档,需要相关的XSL文档和DTD文档。test_report_multiuser-Results
|
|
|
|
|
Running Suite Suite(need not init multiuser data) |
||||
|
Running test test of get_key() ... |
Passed |
||
|
Running test test of original crypt and decrypt ... |
Passed |
||
|
Running test test of read_group() ... |
Passed |
||
|
Running test test of crypt and decrypt ... |
Passed |
||
|
Running test test of mu_load_multiuser_files() ... |
Passed |
||
|
Running test test of test parameter check functions ... |
Passed |
||
|
Running test test of free_list() ... |
Passed |
||
Running Suite Mysuite(need load multiuser data |
||||
|
Running test test of get_admin_info() ... |
Passed |
||
|
Running test test of mu_get_all_user_info() ... |
Passed |
||
|
Running test test of find_user() ... |
Passed |
||
|
Running test test of write_group() ... |
Passed |
||
|
Running test test of mu_login() ... |
Passed |
||
|
Running test test of mu_logout() ... |
Passed |
||
|
Running test test of mu_islogin() ... |
Passed |
||
|
Running test test of mu_user_authentication() ... |
Passed |
||
|
Running test test of mu_modify_password() ... |
Passed |
||
|
Running test test of mu_modify_settings() ... |
Passed |
||
|
Running test test of mu_get_user_settings() ... |
Passed |
||
|
Running test test of mu_register() ... |
Passed |
||
|
Running test test of mu_pop_is_busy() ... |
Passed |
||
|
Running test test of add_to_list and remove_from_list ... |
Passed |
Cumulative Summary for Run |
||||
Type |
Total |
Run |
Succeeded |
Failed |
Suites |
2 |
2 |
- NA - |
0 |
Test Cases |
21 |
21 |
21 |
0 |
Assertions |
5241 |
5241 |
5241 |
0 |
File Generated By CUnit at Mon May 22 11:35:57 2006
表二是关于测试用例的报告:test_report_multiuser-Listing
Total Number of Suites |
2 |
Total Number of Test Cases |
21 |
Suite |
Suite(need not init multiuser data) |
Initialize Function? |
Yes |
Cleanup Function? |
Yes |
Test Count |
7 |
Test Cases |
test
of get_key() |
||||||
|
|||||||
Suite |
Mysuite(need load multiuser data |
Initialize Function? |
Yes |
Cleanup Function? |
Yes |
Test Count |
14 |
Test Cases |
test
of get_admin_info() |
||||||
|
File Generated By CUnit at Mon May 22 11:35:57 2006
对于Cunit本身运行时的错误有可能使我们误认为是被测程序的错误,这样不利于错误的定位,因此Cunit提供了本身的错误处理函数,主要是以下两个:
CU_ErrorCode CU_get_error(void)
const
char* CU_get_error_msg(void)
几乎所有的Cunit函数在发生错误的时候,都会设置相应的错误码,我们可以诊断出到底发生了什么:错误码如下:
-
Error Value
Description
CUE_SUCCESS
No error condition.
CUE_NOMEMORY
Memory allocation failed.
CUE_NOREGISTRY
Test registry not initialized.
CUE_REGISTRY_EXISTS
Attempt to CU_set_registry() without CU_cleanup_registry().
CUE_NOSUITE
A required CU_pSuite pointer was NULL.
CUE_NO_SUITENAME
Required CU_Suite name not provided.
CUE_SINIT_FAILED
Suite initialization failed.
CUE_SCLEAN_FAILED
Suite cleanup failed.
CUE_DUP_SUITE
Duplicate suite name not allowed.
CUE_NOTEST
A required CU_pTest pointer was NULL.
CUE_NO_TESTNAME
Required CU_Test name not provided.
CUE_DUP_TEST
Duplicate test case name not allowed.
CUE_TEST_NOT_IN_SUITE
Test is not registered in the specified suite.
CUE_FOPEN_FAILED
An error occurred opening a file.
CUE_FCLOSE_FAILED
An error occurred closing a file.
CUE_BAD_FILENAME
A bad filename was requested (NULL, empty, nonexistent, etc.).
CUE_WRITE_ERROR
An error occurred during a write to a file.
1:我们应该重视测试程序代码本身的书写。
2:注意测试代码本身也需要进行相应的错误处理和资源回收,否则在后续的测试工程中,使用资源检查工具诊断出来的泄漏通常是测试代码的问题。
3:一定要使用断言,不要使用自定义函数。
4:主要保持测试代码和被测代码的相互独立,尽量不要改动被测试代码。
5:注意单元测试的测试用例的完备性,对于参数的不同情况,需要考虑是否已经将所有的可能全部覆盖。
6:注意测试代码的覆盖性,Cunit仅仅运行测试人员写下的程序,如果你不为被测函数写测试代码,它就不会被运行到。
7:注意保持测试代码的自动化,保证每一次都可以正确运行。比如一个函数设置一个全局变量,仅仅在某些状态下才可以正确执行,那么设置完成,并断言成功之后,需要将这个全局变量恢复到初始值,一方面,可以保证下次执行还是正确的,另一方面,可以减少对其他测试函数的影响。(当然不能一概而论,可以根据具体的情况适用该条款)
8:Cunit用于维持自动的测试环境和测试用例,以便在进行了修改之后,继续检查接口是否发生了变动,以及接口是否正确。他不是一个调试工具,不要将使用debug才可以运行的代码加入进来。