How to Write a Simple UnitTest Framework

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章