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