C开发中的单元测试

最近在写C代码的过程中,感觉自己在重复一项必不可少的环节,就是自测代码,这使我想起以前在写JAVA时的Junit带来的快捷和方便,于是马上行动,经百度、谷歌几轮后,发现Cunit工具,看名字,就可猜到它与Junit同属一宗。网上的相关内容也都非常雷同,这里不再详述,有兴趣的话,可以直奔官方文档:http://cunit.sourceforge.net/doc/index.html

经过仔细观赏,要借用Cunit来提高自己的编码效率和质量,有必要先搞清它的几项要点:
首先仔细观察下图:

可以看出Cunit也是有组织的,呵呵,主要分几个角色,Registry,Suite及Test方法。可以通过下面例子,体会到这种组织关系。
按官方文档说明,使用Cunit的主要步骤有:
1) Write functions for tests (and suite init/cleanup if necessary).
2) Initialize the test registry - CU_initialize_registry()
3) Add suites to the test registry - CU_add_suite()
4) Add tests to the suites - CU_add_test()
5) Run tests using an appropriate interface, e.g. CU_console_run_tests
6) Cleanup the test registry - CU_cleanup_registry

本人英文不咋地,就不献丑翻译了,直接用英文理解吧(要努力用英文).
下面我们结合一个小实例,来体验一下Cunit的便利吧(学编程,最有效的方式还是自己动手)
先编写一个具体两个简单功能的函数,然后写Testcase来测试它。
文件主要有:
1) strformat.h :字符串功能函数的接口文件
2)strformat.c :字符串功能函数的实现
3)testcase.c : 测试用例及Cunit运行环境
4)makefile :
下面直奔代码:
代码:strformat.h

 1 /* strformat.h --- 
 2  * 
 3  * Filename: strformat.h
 4  * Description: 字符串操作头文件
 5  * Author: magc
 6  * Maintainer: 
 7  * Created: 一  8月 20 22:57:19 2012 (+0800)
 8  * Version: 
 9  * Last-Updated: 六  8月 25 10:31:30 2012 (+0800)
10  *           By: magc
11  *     Update #: 15
12  * URL: 
13  * Keywords: 
14  * Compatibility: 
15  * 
16  */
17 
18 /* Commentary: 
19  * 为的是体验Cunit而临时写的几项功能函数,没有多大实际意义,仅仅是为了写测试类
20  * 
21  * 
22  */
23 
24 /* Change Log:
25  * 
26  * 
27  */
28 
29 /* Code: */
30 
31 #ifndef _strformat_h
32 #define _strformat_h
33 
34 typedef char * string;
35 
36 /*************************************************************************
37 *功能描述:返回字符串的长度
38 *参数列表:
39 *返回类型:
40 **************************************************************************/    
41 int string_lenth(string word);
42 /*************************************************************************
43 *功能描述:返回字符串的大写形式
44 *参数列表:
45 *返回类型:
46 **************************************************************************/
47 string to_Upper(string word);
48 /*************************************************************************
49 *功能描述:连接字符串
50 *参数列表:
51 *返回类型:
52 **************************************************************************/
53 string add_str(string word1 ,string word2);
54 
55 
56 
57 #endif
58 
59 
60 /* strformat.h ends here */

 

代码:strformat.c

 1 /* strformat.c --- 
 2  * 
 3  * Filename: strformat.c
 4  * Description: 字符串操作
 5  * Author: magc
 6  * Maintainer: 
 7  * Created: 一  8月 20 22:56:36 2012 (+0800)
 8  * Version: 
 9  * Last-Updated: 六  8月 25 10:33:07 2012 (+0800)
10  *           By: magc
11  *     Update #: 33
12  * URL: 
13  * Keywords: 
14  * Compatibility: 
15  * 
16  */
17 
18 /* Commentary: 
19  * 此代码仅为体验Cunit而临时撰写。
20  * 
21  * 
22  */
23 
24 /* Change Log:
25  * 
26  * 
27  */
28 
29 /* Code: */
30 #include <assert.h>
31 #include <ctype.h>
32 #include <errno.h>
33 #include <limits.h>
34 #include <string.h>
35 #include <stdarg.h>
36 #include <stdlib.h>
37 #include <stdio.h>
38 #include "strformat.h"
39 
40 
41 /**************************************************************************
42 函数名称:字符串相加
43 功能描述:
44 输入参数:
45 返   回:
46 **************************************************************************/
47 string add_str(string word1 ,string word2){
48     return (strcat(word1, word2));
49 }
50 
51 /**************************************************************************
52 函数名称:将字符串转换成大写格式
53 功能描述:
54 输入参数:
55 返   回:
56 **************************************************************************/
57 string to_Upper(string word){
58     int i;
59     for(i = 0;word[i] !='\0' ;i++){
60         if(word[i]<'z' && word[i]>'a'){
61             word[i] -= 32;
62         }
63     }
64     return word;
65     
66 }
67 
68 /**************************************************************************
69 函数名称:字符串长度
70 功能描述:
71 输入参数:
72 返   回:
73 **************************************************************************/
74 int string_lenth(string word){
75     int i;
76     for(i = 0 ;word[i] != '\0';i++){
77         
78     }
79     return i;
80 }
81 
82 /* strformat.c ends here */

 

测试代码: testcase.c

  1 /* testcase.c --- 
  2  * 
  3  * Filename: testcase.c
  4  * Description: 测试实例 
  5  * Author: magc
  6  * Maintainer: 
  7  * Created: 一  8月 20 23:08:53 2012 (+0800)
  8  * Version: 
  9  * Last-Updated: 五  8月 24 16:09:40 2012 (+0800)
 10  *           By: magc
 11  *     Update #: 135
 12  * URL: 
 13  * Keywords: 
 14  * Compatibility: 
 15  * 
 16  */
 17 
 18 /* Commentary:
 19  * 当前文件用来定义测试方法,suite,及registry信息,若测试方法有变化,只需要修改当前文件即可。
 20  * 第一步:书写测试函数的代码,建议以"test_"为前缀。
 21  * 第二步:将测试方法归类,即将相似功能的测试方法放到一个数组里,以便把它们指定给一个suite
 22  * 第三步:创建suite,可按功能或模块,生成多个test suite,
 23  * 第四步:书写测试方法的总调用方法,AddTests(),用来统一启动测试方法。
 24  */
 25 
 26 /* Change Log:
 27  * 
 28  * 
 29  */
 30 
 31 /* Code: */
 32 #include <assert.h>
 33 #include <ctype.h>
 34 #include <errno.h>
 35 #include <limits.h>
 36 #include <string.h>
 37 #include <stdarg.h>
 38 #include <stdlib.h>
 39 #include <stdio.h>
 40 
 41 #include <CUnit/Basic.h>
 42 #include <CUnit/Console.h>
 43 #include <CUnit/CUnit.h>
 44 #include <CUnit/TestDB.h>
 45 #include "strformat.h"
 46 
 47 /**************************************************************************
 48 函数名称:测试string_lenth()方法
 49 功能描述:
 50 输入参数:
 51 返   回:
 52 **************************************************************************/
 53 void test_string_lenth(void){
 54     string test = "Hello";
 55     int len = string_lenth(test);
 56     CU_ASSERT_EQUAL(len,5);
 57 }
 58 
 59 /**************************************************************************
 60 函数名称:测试方法to_Upper()
 61 功能描述:
 62 输入参数:
 63 返   回:
 64 **************************************************************************/
 65 
 66 void test_to_Upper(void){
 67     char test[] = "Hello";
 68     CU_ASSERT_STRING_EQUAL(to_Upper(test),"HELLO");
 69     
 70 }
 71 
 72 /**************************************************************************
 73 函数名称:测试方法 add_str()
 74 功能描述:
 75 输入参数:
 76 返   回:
 77 **************************************************************************/
 78 void test_add_str(void){
 79     char test1[] = "Hello!";
 80     char test2[] = "MGC";
 81     CU_ASSERT_STRING_EQUAL(add_str(test1,test2),"Hello!MGC");
 82     
 83 }
 84 
 85 /**************************************************************************
 86 数组名称:将多个测试方法打包成组,以供指定给一个Suite
 87 功能描述:每个suite可以有一个或多个测试方法,以CU_TestInfo数组形式指定
 88 **************************************************************************/
 89 // CU_TestInfo是Cunit内置的一个结构体,它包括测试方法及描述信息
 90 CU_TestInfo testcase[] = {
 91     {"test_for_lenth:",test_string_lenth    },
 92     {"test_for_add:",test_add_str    },
 93     CU_TEST_INFO_NULL
 94 };
 95 
 96 CU_TestInfo testcase2[] = {
 97     {"test for Upper :",test_to_Upper    },
 98     CU_TEST_INFO_NULL
 99 };
100 
101 /**************************************************************************
102 函数名称:suite初始化过程
103 功能描述:
104 输入参数:
105 返   回:
106 **************************************************************************/
107 int suite_success_init(void){
108     return 0;
109     
110 }
111 
112 /**************************************************************************
113 函数名称:suite清理过程,以便恢复原状,使结果不影响到下次运行
114 功能描述:
115 输入参数:
116 返   回:
117 **************************************************************************/
118 int suite_success_clean(void){
119     return 0;
120 }
121 
122 //定义suite数组,包括多个suite,每个suite又会包括若干个测试方法。
123 CU_SuiteInfo suites[] = {
124     {"testSuite1",suite_success_init,suite_success_clean,testcase},
125     {"testsuite2",suite_success_init,suite_success_clean,testcase2},
126     CU_SUITE_INFO_NULL
127 };
128 
129 /**************************************************************************
130 函数名称:测试类的调用总接口
131 功能描述:
132 输入参数:
133 返   回:
134 **************************************************************************/
135 void AddTests(){
136     assert(NULL != CU_get_registry());
137     assert(!CU_is_test_running());
138 
139     if(CUE_SUCCESS != CU_register_suites(suites)){
140         exit(EXIT_FAILURE);
141 
142     }
143 }
144 /*************************************************************************
145 *功能描述:运行测试入口
146 *参数列表:
147 *返回类型:
148 **************************************************************************/
149 
150 int RunTest(){
151         if(CU_initialize_registry()){
152                 fprintf(stderr, " Initialization of Test Registry failed. ");
153                 exit(EXIT_FAILURE);
154         }else{
155                 AddTests();
156                 /**** Automated Mode *****************
157                 CU_set_output_filename("TestMax");
158                 CU_list_tests_to_file();
159                 CU_automated_run_tests();
160                 //************************************/
161                 
162                 /***** Basice Mode *******************
163                 CU_basic_set_mode(CU_BRM_VERBOSE);
164                 CU_basic_run_tests();
165                 //************************************/
166 
167                 /*****Console Mode ********************
168                 CU_console_run_tests();
169                 //************************************/
170 
171                 CU_cleanup_registry();
172                 
173                 return CU_get_error();
174                 
175         }
176 
177 }
178 /*************************************************************************
179 *功能描述:测试类主方法
180 *参数列表:
181 *返回类型:
182 **************************************************************************/
183 
184 int main(int argc, char * argv[])
185 {
186    return  RunTest();
187     
188 }
189 
190 
191 
192 
193 
194 /* testcase.c ends here */

注:
1)注意结合上面Cunit的组织结构图,理解Cunit中几个角色的关系(CU_TestInfo,CU_SuiteInfo各以数组的形式,将多个Test和Suite组织起来)。
2)Cunit有几种运行模式,如automated,basic,console,有的是可以交互的,有的是没有交互,直接出结果的。

 

代码:makefile

IINC=-I/usr/local/include/CUnit
LIB=-L/usr/local/lib/

all:  strformat.c testcase.c
    gcc -o test $(INC) $(LIB)  $^ -lcunit -static

注:
1)Cunit安装很简单,从官方地址上下载源代码后,在本机依次执行
./configure
make 
sudo make install
安装成功后相关的库及头文件安装到默认路径下。编译时添加选项:
-I/usr/local/include/CUnit
-L/usr/local/lib/
就如makefile中的一样。

 


下面我们欣赏一下Cunit的常见几种运行模式
1)Automated Mode
先将testcase.c中156~159代码放开注释,此时便是以automated模式运行,此模块没有交互能力,直接生成XML格式的报表,先make,然后运行后,在当前目录下生成两个报表
TestMax-Listing.xml和TestMax-Results.xml(前者是测试用例的列表,后者是测试用例的测试结果) ,但这两个文件是不能直接观看的,要查看这两个文件,需要使用如下xsl和dtd文件:CUnit-List.dtd和CUnit-List.xsl用于解析列表文件, CUnit-Run.dtd和CUnit-Run.xsl用于解析结果文件。这四个文件在CUnit包里面有提供,安装之后在$(PREFIX) /share/CUnit目录下,默认安装的话在/home/lirui/local/share/CUnit目录下。在查看结果之前,需要把这六 个文件:TestMax-Listing.xml, TestMax-Results.xml, CUnit-List.dtd, CUnit-List.xsl, CUnit-Run.dtd, CUnit-Run.xsl拷贝到一个目录下,然后用浏览器打开两个结果的xml文件就可以了。
如下图所示:

 

 

2) Console Mode
在testcase.c中将其它模式代码注释掉,放开168行代码,便转换成Console模式了。console模式支持交互,如支持运行,查看错误等操作,如下图所示:

从上图即可看出,输入R来运行,输入F来查看错误用例,输入Q来退出

这种模式在实际中是很实用的。

3)Basic Mode
在testcase.c中将其它模式的代码注释掉,放到163~164行代码,便转换成Basic模式,这种是不支持交互的,直接打印出运行结果。

可以看出,这种模式也是比较简单快捷的

 


另外对于这种写测试代码的重复工作,可以想办法减小重复,还好,我用的是Emacs写代码,可以借助强大的msf-abbrev 来定义一个testcase的模板,每次写测试时,可以直接引入,既简单又快捷,使自己将有限的精力集中到更核心的部分。(假如你不知我在说什么,就当没看到这部分,直接闪过)

 


小结:
以后写代码过程中,若需要测试函数的功能,就可以采用如下步骤:
1)创建一个专门的测试类,用msf-abbrev模板快捷创建内容,
2) 添加测试函数
3) 将测试函数按组放到CU_TestInfo数组中,并指定给一个Suite
4)根据自己需要,定义CU_SuiteInfo数组。
5)在RunTest()中定义运行模式。
6)在main函数中调用RunTest(),默认生成的testcase中有一个main函数,若不与其它冲突,直接在这里调用即可。(若main冲突,则砍掉不需要的那个)

 

代码附件