How to Write a Simple UnitTest Framework

Table of Contents

很多时候, 我们需要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:

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