personal blog: http://finaldie.com/wordpress/
email: hyzwowtools@gmail.com
很多时候, 我们需要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