CuTest是一款微小的C語言單元測試框,是我迄今爲止見到的最簡潔的測試框架之一,只有2個文件,CuTest.c和CuTest.h,所有代碼加起來不到一千行。麻雀雖小,五臟俱全,測試的構建、測試的管理、測試語句,都所有包含在內。linux
一個測試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返回。測試
不管設計多麼精妙的測試,都須要一個一個的邏輯測試函數,這就是測試case。好比下面的測試case。
待測函數原型:ui
int AddInt(int a, int b);
測試用例:spa
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());
測試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; }
下面是一個簡單的實例,包含了測試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); }