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冲突,则砍掉不需要的那个)