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沖突,則砍掉不需要的那個)