personal blog: http://finaldie.com/wordpress/
email: [email protected]
很多時候, 我們需要UnitTest幫助我們快速的發現代碼修改中引發的問題, UnitTest的意義以及重要性已經無需重複, 那麼在實際項目中, 我們會選取合適的UnitTest Framework幫助我們完成這項工作, 然而UnitTest Framework也有很多種, 挑選的時候大多根據項目需要, 不過大家是否有衝動自己寫一個那? 來一探UnitTest Framework的究竟(本文將實現一個C語言的UnitTest Framework 代碼放置在https://github.com/finaldie/final_libs的ftu中).
原理:
UnitTest Framework通常幫助我們完成以下幾種功能:
1. 提供常用assert API
2. 註冊執行test case
3. 生成report
關於斷言, 我們通常使用幾種形式的斷言, 比如:
1. 某個值是否於期望值相等
2. 某個值是否大於期望值
3. 某個值是否小於期望值
所以, 如果我們自己來寫一個, 只需要提供基本的assert API, 註冊和執行的API即可(最後的報告放在run API內部即可).
WorkFlow:
實現:
原理清楚了, 實現起來就很容易了. 首先我們先來提供幾個基本的assert API:
extern int curr_failed_assert;
extern int curr_total_assert;
#define FTU_ASSERT_EQUAL_CHAR(expect, real) \
do{ curr_total_assert++; if( strcmp(expect, real) ) { printf("(%s %s) %d: ASSERT FAILED, expect=%s but real=%s \n", __FILE__, __func__, __LINE__, expect, real); curr_failed_assert++; } }while(0)
#define FTU_ASSERT_EQUAL_INT(expect, real) \
do{ curr_total_assert++; if( expect != real ) { printf("(%s %s) %d: ASSERT FAILED, expect=%d but real=%d \n", __FILE__, __func__, __LINE__, expect, real); curr_failed_assert++; } }while(0)
#define FTU_ASSERT_EQUAL_DOUBLE(expect, real) \
do{ curr_total_assert++; if( fabs(expect - real) < 0.0000001 ) { printf("(%s %s) %d: ASSERT FAILED, expect=%f but real=%f \n", __FILE__, __func__, __LINE__, expect, real); curr_failed_assert++; } }while(0)
#define FTU_ASSERT_GREATER_THAN_INT(expect, real) \
do{ curr_total_assert++; if( real < expect ) { printf("(%s %s) %d: ASSERT FAILED, expect > %d but real=%d \n", __FILE__, __func__, __LINE__, expect, real); curr_failed_assert++; } }while(0)
#define FTU_ASSERT_LESS_THAN_INT(expect, real) \
do{ curr_total_assert++; if( real > expect ) { printf("(%s %s) %d: ASSERT FAILED, expect < %d but real=%d \n", __FILE__, __func__, __LINE__, expect, real); curr_failed_assert++; } }while(0)
#define FTU_ASSERT_EXPRESS(express) \
do{ curr_total_assert++; if( !(express) ) { printf("(%s %s) %d: ASSERT FAILED, expect=%s but failed \n", __FILE__, __func__, __LINE__, #express); curr_failed_assert++; } }while(0)
這裏面有2個特別的變量 curr_total_assert 和 curr_failed_assert, 稍微解釋一下其作用, 通常我們不但希望這些assert API可以提供常規的斷言檢查, 還希望提供比如當前test case中 "一共執行了多少assert", "失敗了多少個", 所以這兩個變量相當於統計這些計數, 這樣可以讓我們的report變得更加直觀, 明良 :)
接下來我們再提供基本的註冊和運行接口:
1. 註冊接口: 我們需要將test case函數註冊進framework中, 所有的case信息可以用一個鏈表串起來, 執行的時候按順序執行即可 :), 這裏面有一個問題, 這個鏈表需要初始化, 所以我們可以提供一個init API以便初始化鏈表, 也可以將初始化工作放在註冊和執行接口內部, 每次執行的時候檢查一下是否已經初始化好了, 所有的test case都是串行執行, 沒有鎖爭用和並行問題. 這裏面, 我是顯示的提供了一個init API, 代碼如下:
typedef void (*pfunc_init)(); // function type of test case
typedef struct {
pfunc_init pfunc;
char* case_name;
char* describe;
}ftest_case;
void tu_register_init(){
if( plist ) return;
plist = flist_create();
tu_case_num = 0;
failed_cases = 0;
curr_failed_assert = 0;
curr_total_assert = 0;
}
void _tu_register_module(pfunc_init pfunc, char* case_name, char* describe){
tu_case_num++;
ftest_case* ftc = (ftest_case*)malloc(sizeof(ftest_case));
ftc->pfunc = pfunc;
ftc->case_name = case_name;
ftc->describe = describe;
flist_push(plist, ftc);
}
2. 執行接口: 這個函數的功能很簡單, 就是按順序逐個的取得已經註冊好的test case 並執行, 最終統計各個assert狀態並輸出report.
static int tu_each_case(pfunc_init pfunc)
{
curr_failed_assert = 0;
curr_total_assert = 0;
// run test case
pfunc();
if( curr_failed_assert ) {
failed_cases++;
}
return 0;
}
void tu_run_cases()
{
printf("FINAL TEST UNIT START...\n");
ftest_case* ftc = NULL;
while( ( ftc = (ftest_case*)flist_pop(plist) ) ){
printf("\n <<<<<<< CASE NAME:%s DESCRIBE:%s >>>>>>>\n", ftc->case_name, ftc->describe ? ftc->describe : "");
tu_each_case(ftc->pfunc);
free(ftc);
if ( curr_failed_assert ) {
printf("[%d ASSERT FAILED -- %d/%d]\n",
curr_failed_assert,
curr_total_assert,
curr_total_assert - curr_failed_assert);
}
else {
printf("[ALL ASSERT PASSED -- %d/%d]\n",
curr_total_assert,
curr_total_assert);
}
}
printf("\n--------------------------------------\nTOTAL CASE %d, PASS %d, FAILED %d\n",
tu_case_num,
tu_case_num - failed_cases,
failed_cases);
}
可以看到, printf輸出的都是report的一些基本分割線和case name, total_case_num之類的, 這些可以根據自己的喜好進行添加, 不在討論範圍內了, 我們大可不關心上面printf語句, 核心的語句只有 while(...) { tu_each_case(...) }.
OK, 這樣一個UnitTest的框架就搭好了, 是不是很簡單, 或許我們並沒有做的像很成熟的框架那樣面面俱到, 不過通過編寫一個簡單框架, 我們可以很快速的理解UnitTest Framework內部構造和原理, 方便更好的理解和協調我們的工作. :)
DEMO:
框架我們已經搭好了, 現在我們開始利用這些API運行一個簡單的case.
#include "tu_inc.h"
static void func1()
{
return 1;
}
static void test_case()
{
int ret = func1();
FTU_ASSERT_EQUAL_INT(0, 1);
}
int main(int argc, char** argv){
tu_register_init();
tu_register_module(test_case, "for test");
tu_run_cases();
return 0;
}
編譯運行, 你將會看到預期結果:
<<<<<<< CASE NAME:test_case DESCRIBE:for test >>>>>>>
(main.c test_case) 10: ASSERT FAILED, expect = 0 but real=1
[1 ASSERT FAILED -- 1/1]
--------------------------------------
TOTAL CASE 1, PASS 0, FAILED 1