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); }