1、简介
CuTest是一款微小的C语言单元测试框,是我迄今为止见到的最简洁的测试框架之一,只有2个文件,CuTest.c和CuTest.h,全部代码加起来不到一千行。麻雀虽小,五脏俱全,测试的构建、测试的管理、测试语句,都全部包含在内。
2、CuTest剖析
2.1 断言
一个测试case是否通过落到代码实处,就是对测试值与期待值之间进行比较,这就要用到断言。
#define CuAssertStrEquals(tc,ex,ac) CuAssertStrEquals_LineMsg((tc),__FILE__,__LINE__,NULL,(ex),(ac)) #define CuAssertStrEquals_Msg(tc,ms,ex,ac) CuAssertStrEquals_LineMsg((tc),__FILE__,__LINE__,(ms),(ex),(ac)) #define CuAssertIntEquals(tc,ex,ac) CuAssertIntEquals_LineMsg((tc),__FILE__,__LINE__,NULL,(ex),(ac)) #define CuAssertIntEquals_Msg(tc,ms,ex,ac) CuAssertIntEquals_LineMsg((tc),__FILE__,__LINE__,(ms),(ex),(ac)) #define CuAssertDblEquals(tc,ex,ac,dl) CuAssertDblEquals_LineMsg((tc),__FILE__,__LINE__,NULL,(ex),(ac),(dl)) #define CuAssertDblEquals_Msg(tc,ms,ex,ac,dl) CuAssertDblEquals_LineMsg((tc),__FILE__,__LINE__,(ms),(ex),(ac),(dl)) #define CuAssertPtrEquals(tc,ex,ac) CuAssertPtrEquals_LineMsg((tc),__FILE__,__LINE__,NULL,(ex),(ac)) #define CuAssertPtrEquals_Msg(tc,ms,ex,ac) CuAssertPtrEquals_LineMsg((tc),__FILE__,__LINE__,(ms),(ex),(ac))
......
......
以数字测试为例CuAssertIntEquals,其实现为:
void CuAssertIntEquals_LineMsg(CuTest* tc, const char* file, int line, const char* message, int expected, int actual) { char buf[STRING_MAX]; if (expected == actual) return; sprintf(buf, "expected <%d> but was <%d>", expected, actual); CuFail_Line(tc, file, line, message, buf); }
如果测试成功,则会安静的进行下一步,由return返回此函数。
大部分的测试框架的哲学和linux哲学很像,小即是美,少就是好,没有异常下不会打扰用户。
而万一出现错误,则会保存错误信息,还有文件路径/文件名/函数名、及行号。
sprintf(buf, "expected <%d> but was <%d>", expected, actual); CuFail_Line(tc, file, line, message, buf);
继续深入,上面函数实现了:拼接错误消息到string,然后传递给CuFailInternal函数。很容易从CuFailInternal函数名发现,这个函数才是真正的错误返回的核心。
1)把函数名和行号,追加到用户错误消息的字符串后面。由CuStringInsert语句实现。
2)错误标志,tc->failed置位。
3)完整的错误消息引用赋值给测试的消息指针。
4)返回,长跳转。
void CuFail_Line(CuTest* tc, const char* file, int line, const char* message2, const char* message) { CuString string; CuStringInit(&string); if (message2 != NULL) { CuStringAppend(&string, message2); CuStringAppend(&string, ": "); } CuStringAppend(&string, message); CuFailInternal(tc, file, line, &string); } static void CuFailInternal(CuTest* tc, const char* file, int line, CuString* string) { char buf[HUGE_STRING_LEN]; sprintf(buf, "%s:%d: ", file, line); CuStringInsert(string, buf, 0); tc->failed = 1; tc->message = string->buffer; if (tc->jumpBuf != 0) longjmp(*(tc->jumpBuf), 0); }
到这里,一个错误的测试就会从longjmp返回。
2.2 测试的组织
无论设计多么精妙的测试,都需要一个一个的逻辑测试函数,这就是测试case。比如下面的测试case。
待测函数原型:
int AddInt(int a, int b);
测试用例:
void test_add(CuTest* tc) { CuAssert(tc, "\r\ntest not pass", 2 == AddInt(1,0); } CuSuite* TestAdd(void) { CuSuite* suite = CuSuiteNew(); SUITE_ADD_TEST(suite, test_add); return suite; }
如果有许多测试,则要用到测试组的管理。也就是测试case的管理,CuTest中叫做suite。
CuSuite* CuGetSuite(void) { CuSuite* suite = CuSuiteNew(); SUITE_ADD_TEST(suite, TestCuStringAppendFormat); SUITE_ADD_TEST(suite, TestCuStrCopy); SUITE_ADD_TEST(suite, TestFail); SUITE_ADD_TEST(suite, TestAssertStrEquals); SUITE_ADD_TEST(suite, TestAssertStrEquals_NULL); return suite; }
一般而言suite是一类测试的集合,其实就是调用了CuSuiteAdd函数。
#define SUITE_ADD_TEST(SUITE,TEST) CuSuiteAdd(SUITE, CuTestNew(#TEST, TEST))
用宏展开,#TEST等价于TEST内容转换为字符串,CuTestNew(#TEST, TEST)是宏的一种妙用。此函数作用是把case加入到testSuite的具体链表中去。
void CuSuiteAdd(CuSuite* testSuite, CuTest *testCase) { assert(testSuite->count < MAX_TEST_CASES); testSuite->list[testSuite->count] = testCase; testSuite->count++; }
上面是一类测试,用suite函数SUITE_ADD_TEST来实现多个测试函数的归类管理。那么有多个的函数的测试时候,是如何规划呢,需要suite上再添加suite了。最后对上层接口提供一个总的suite的引用即可。
CuSuite* suite = CuSuiteNew();
CuSuiteAddSuite(suite, CuGetSuite());
CuSuiteAddSuite(suite, CuStringGetSuite());
CuSuiteAddSuite(suite, TestAdd());
2.3 测试的运行
测试case构成了测试组--suite,然后多个测试组可以合并为一个测试组。测试组的执行就是遍历数组,执行内部的每一个测试case。
void CuSuiteRun(CuSuite* testSuite) { int i; for (i = 0 ; i < testSuite->count ; ++i) { CuTest* testCase = testSuite->list[i]; CuTestRun(testCase); if (testCase->failed) { testSuite->failCount += 1; } } }
测试的执行靠CuTestRun来完成,依旧是打下跳转断点--setjmp(buf),然后运行测试case,如果测试case无错误,则安静的退出,否则记录出错信息,然后longjmp返回到if (setjmp(buf) == 0)一行,在CuSuiteRun中,会对错误case的个数进行计数,以便全部case运行完毕后,输出总结信息用。
void CuTestRun(CuTest* tc) { jmp_buf buf; tc->jumpBuf = &buf; if (setjmp(buf) == 0) { tc->ran = 1; (tc->function)(tc); } tc->jumpBuf = 0; }
上面的函数,测试函数的调用很隐晦,是(tc->function)(tc)语句完成的。测试case的原型为:
typedef void (*TestFunction)(CuTest *); struct CuTest { char* name; TestFunction function; int failed; int ran; const char* message; jmp_buf *jumpBuf; };
所以function就指向具体的测试case。
具体的实现为:第一步创建测试case,即CuTest* tc。CuTestNew传入的参数function就是具体测试case函数的引用指针。
CuTest* CuTestNew(const char* name, TestFunction function) { CuTest* tc = CU_ALLOC(CuTest); CuTestInit(tc, name, function); return tc; }
第二步,测试case初始化,将funciton引用指针赋值给CuTest* t->function。所以(tc->function)(tc)语句就相当于直接调用测试case函数本体。
void CuTestInit(CuTest* t, const char* name, TestFunction function) { t->name = CuStrCopy(name); t->failed = 0; t->ran = 0; t->message = NULL; t->function = function; t->jumpBuf = NULL; }
3、CuTest实例
下面是一个简单的实例,包含了测试case,测试组,测试执行。
1)测试case
void test_add(CuTest* tc) { CuAssert(tc, "\r\ntest not pass", 2 == 1 + 1); }
2)测试组suite
CuSuite* TestAdd(void) { CuSuite* suite = CuSuiteNew(); SUITE_ADD_TEST(suite, test_add); return suite; }
3)测试项目结构组织
void main() { RunAllTests(); getchar(); } void RunAllTests(void) { CuString *output = CuStringNew(); CuSuite* suite = CuSuiteNew(); CuSuiteAddSuite(suite, TestAdd()); CuSuiteRun(suite); CuSuiteSummary(suite, output); CuSuiteDetails(suite, output); printf("%s\n", output->buffer); }