1、簡介
Embedded Unit是個純標准c構建的單元測試框架,主要用在嵌入式c的單體測試上,其主要特點是不依賴於任何C的標准庫,所有的對象都是靜態分配。
最早這個項目托管在SourceForge上(https://sourceforge.net/projects/embunit ),目前在GitHub也有多個拷貝。
2、框架剖析
2.1 斷言
#define TEST_ASSERT_NULL(pointer)\ TEST_ASSERT_MESSAGE(pointer == NULL,#pointer " was not null.") #define TEST_ASSERT_NOT_NULL(pointer)\ TEST_ASSERT_MESSAGE(pointer != NULL,#pointer " was null.") #define TEST_ASSERT_MESSAGE(condition, message)\ if (condition) {} else {TEST_FAIL(message);} #define TEST_ASSERT(condition)\ if (condition) {} else {TEST_FAIL(#condition);} #define TEST_FAIL(message)\ if (0) {} else {addFailure(message,__LINE__,__FILE__);return;
TEST_ASSERT_NULL依賴TEST_ASSERT_MESSAGE,TEST_ASSERT_MESSAGE依賴TEST_FAIL,TEST_FAIL依賴addFailure。
所以一般的錯誤斷言,會使用addFailure來完成錯誤處理,其原型如下。
void addFailure(const char *msg, long line, const char *file) { TestResult_addFailure(result_, (Test*)self_, (char*)msg, line, (char*)file); } void TestResult_addFailure(TestResult* self,Test* test,const char* msg,int line,const char* file) { self->failureCount++; if (self->listener) { TestListner_addFailure(self->listener, test, msg, line, file); } }
在TestResult_addFailure中對錯誤case的總數進行計數,而錯誤消息由TestListner_addFailure負責。
static void TestRunner_addFailure(TestListner* self,Test* test,char* msg,int line,char* file) { stdimpl_print("\n"); stdimpl_print(Test_name(root_)); stdimpl_print("."); stdimpl_print(Test_name(test)); { char buf[16]; stdimpl_print(" ("); stdimpl_print(file); stdimpl_print(" "); stdimpl_itoa(line, buf, 10); stdimpl_print(buf); stdimpl_print(") "); } stdimpl_print(msg); stdimpl_print("\n"); }
2.2 測試case管理
EmbedUnit在測試的管理方面,主要使用了2個編程技術,一是結構體數組、二是函數指針。EmbedUnit可以說是C語言模塊化開發的教材,在宏定義、函數指針、結構體對象方面的應用十分精妙。
TestRef CounterTest_tests(void) { EMB_UNIT_TESTFIXTURES(fixtures) { new_TestFixture("testInit",testInit), new_TestFixture("testSetValue",testSetValue), new_TestFixture("testInc",testInc), new_TestFixture("testDec",testDec), new_TestFixture("testClr",testClr), }; EMB_UNIT_TESTCALLER(CounterTest,"CounterTest",setUp,tearDown,fixtures); return (TestRef)&CounterTest; }
EMB_UNIT_TESTFIXTURES(fixtures)很奇怪的C語言寫法,但是如果展開后就很明了恍然大悟。
#define EMB_UNIT_TESTFIXTURES(fixtures) \ static const TestFixture fixtures[] = #define new_TestFixture(name,test)\ {\ name,\ test,\ }
fixtures就是一個數組而已,static const TestFixture fixtures[]。new_TestFixture就是一個大括號。
然后是關鍵的一句EMB_UNIT_TESTCALLER,這個函數把上面的數組fixtures[]加入到測試case組,組名叫做CounterTest。 而測試case的個數由sizeof(fixtures)/sizeof(fixtures[0])來直接計算出來。
#define EMB_UNIT_TESTCALLER(caller,name,sup,tdw,fixtures) \ static const TestCaller caller = new_TestCaller(name,sup,tdw,sizeof(fixtures)/sizeof(fixtures[0]),(TestFixture*)fixtures)
繼續深入,new_TestCaller是一個宏定義,展開后擴展為一個TestCaller類型的結構體。
#define new_TestCaller(name,sup,tdw,numberOfFixtuers,fixtuers)\ {\ (TestImplement*)&TestCallerImplement,\ name,\ sup,\ tdw,\ numberOfFixtuers,\ fixtuers,\ }
其結構體定義為:
typedef struct __TestCaller TestCaller; typedef struct __TestCaller* TestCallerRef;/*downward compatible*/ struct __TestCaller { TestImplement* isa; char *name; void(*setUp)(void); void(*tearDown)(void); int numberOfFixtuers; TestFixture *fixtuers; };
上面的寫法非常精妙,值得在項目中學習,第一用宏定義展開結構體很好的包裝了細節。第二結構體類型的使用,不直接用結構體定義名稱__TestCaller,而進行轉換用typedef重新定義為TestCaller,在很大的程度上起到接口隔離的效果。
到目前為止,已經構成了一個完整的測試組,包括setUp,tearDown,fixtuers,測試環境准備、現場清理、待測函數三個因素已經具備。CounterTest類型為TestCaller,被返回傳遞給測試執行函數。
2.3測試的執行
測試的執行得從測試組開始說起,測試組保證了測試例程以及其運行相關的結構數據。 測試的執行從TestRunner_runTest(CounterTest_tests())開始。
void TestRunner_runTest(Test* test) { root_ = test; Test_run(test, &result_); }
對Test_run進行追蹤。
#define Test_run(s,r) ((Test*)s)->isa->run(s,r) struct __Test { TestImplement* isa; };
測試組的執行時從Test_run開始的,參數是Test* test和TestResult result_,與其說TestImplement* isa被轉成(Test*)類型,不如說取出了TestCaller結構體的第一個元素,然后調用了run函數指針。
typedef struct __TestImplement TestImplement; typedef struct __TestImplement* TestImplementRef;/*downward compatible*/ typedef char*(*TestNameFunction)(void*); typedef void(*TestRunFunction)(void*,TestResult*); typedef int(*TestCountTestCasesFunction)(void*); struct __TestImplement { TestNameFunction name; TestRunFunction run; TestCountTestCasesFunction countTestCases; };
這是一路漫長的C面向對象寫法,雖然看起來結構整齊,但是邏輯上繞了很多彎。分析如下。
1)isa->run的來源
TestCaller中的isa來源於定義測試組時候的結構體展開。 TestCallerImplement是一個全局的變量。 在TestCaller 內部,TestCallerImplement是一個全局的變量是其第一個元素,類型為(TestImplement*),也叫做Test類型。
extern const TestImplement TestCallerImplement; #define new_TestCaller(name,sup,tdw,numberOfFixtuers,fixtuers)\ {\ (TestImplement*)&TestCallerImplement,\ name,\ sup,\ tdw,\ numberOfFixtuers,\ fixtuers,\ } struct __Test { TestImplement* isa; };
2)函數的調用
struct __TestImplement { TestNameFunction name; TestRunFunction run; TestCountTestCasesFunction countTestCases; }; const TestImplement TestCallerImplement = { (TestNameFunction) TestCaller_name, (TestRunFunction) TestCaller_run, (TestCountTestCasesFunction)TestCaller_countTestCases, };
所以isa->run就是調用TestCaller_run函數。
typedef void(*TestRunFunction)(void*,TestResult*); void TestCaller_run(TestCaller* self,TestResult* result) { TestCase cs = new_TestCase(0,0,0,0); int i; cs.setUp= self->setUp; cs.tearDown = self->tearDown; for (i=0; i<self->numberOfFixtuers; i++) { cs.name = self->fixtuers[i].name; cs.runTest = self->fixtuers[i].test; /*run test*/ Test_run(&cs,result); } }
更具isa->run(s,r),可以知道,s就是TestCaller 類型的CounterTest變量,只不過在函數調用時候被截取了第一個元素,轉換成了(TestImplement *)類型。
r就是static TestResult result_,用來記錄測試結果。
struct __TestResult { unsigned short runCount; unsigned short failureCount; TestListner* listener; };
到目前為止,所有的測試都從Test_run(test, &result_)跳轉到測執行函數。
3)函數的執行
在TestCaller_run中,Test_run負責執行具體的函數體。
for (i=0; i<self->numberOfFixtuers; i++) { cs.name = self->fixtuers[i].name; cs.runTest = self->fixtuers[i].test; /*run test*/ Test_run(&cs,result); }
cs.runTest = self->fixtuers[i].test負責找到具體的case,Test_run負責執行測試,將其展開。
#define Test_run(s,r) ((Test*)s)->isa->run(s,r)
此處的s是指測試case cs,源於TestCase cs = new_TestCase(0,0,0,0)。
typedef struct __TestCase TestCase; typedef struct __TestCase* TestCaseRef;/*compatible embUnit1.0*/ struct __TestCase { TestImplement* isa; char *name; void(*setUp)(void); void(*tearDown)(void); void(*runTest)(void); };
而此處的((Test*)s)->isa->run(s,r),其中run函數指向誰呢?玄機在TestCase cs = new_TestCase(0,0,0,0); new_TestCase 的第一個元素就是TestCaseImplement。
struct __TestCase { TestImplement* isa; char *name; void(*setUp)(void); void(*tearDown)(void); void(*runTest)(void); }; extern const TestImplement TestCaseImplement; #define new_TestCase(name,setUp,tearDown,runTest)\ {\ (TestImplement*)&TestCaseImplement,\ name,\ setUp,\ tearDown,\ runTest,\ }
這個原型為:
struct __TestImplement { TestNameFunction name; TestRunFunction run; TestCountTestCasesFunction countTestCases; }; const TestImplement TestCaseImplement = { (TestNameFunction) TestCase_name, (TestRunFunction) TestCase_run, (TestCountTestCasesFunction)TestCase_countTestCases, };
測試函數執行,就是TestRunFunction run所指的TestCase_run函數。前面已經由cs.runTest = self->fixtuers[i].test這一句找到函數的應用,然后self->runTest()就是執行該測試函數。
由於不依靠任何c標准庫,所以沒有longjmp這樣的長跳轉,那么測試出錯如何進行返回呢?訣竅就在addFailure函數的時機、以及下面幾個PUSH和POP上,共同完成局部變量和全局的result之間的信息傳遞。
void TestCase_run(TestCase* self,TestResult* result) { TestResult_startTest(result, (Test*)self); if (self->setUp) { self->setUp(); } if (self->runTest) { TestResult* wr =result_; /*push*/ TestCase* ws = self_; /*push*/ result_ = result; self_ = self; self->runTest(); result_ = wr; /*pop*/ self_ = ws; /*pop*/ } if (self->tearDown) { self->tearDown(); } TestResult_endTest(result, (Test*)self); }
3、測試實例
下面演示了一個EmbedUnit的測試工程,包含三個方面:
1. 寫測試例子
比如static void testInit(void)。
2. 構成測試組
比如TestRef CounterTest_tests(void)。返回(TestRef)&CounterTest變量。
3. 調用框架執行全部測試
main函數里面流程的就是測試框架的執行流程。
TestRef CounterTest_tests(void); TestRef PersonTest_tests(void); int main (int argc, const char* argv[]) { TestRunner_start(); TestRunner_runTest(CounterTest_tests()); TestRunner_runTest(PersonTest_tests()); TestRunner_end(); getchar(); return 0; } TestRef CounterTest_tests(void) { EMB_UNIT_TESTFIXTURES(fixtures) { new_TestFixture("testInit",testInit), new_TestFixture("testSetValue",testSetValue), }; EMB_UNIT_TESTCALLER(CounterTest,"CounterTest",setUp,tearDown,fixtures); return (TestRef)&CounterTest; } static void testInit(void) { TEST_ASSERT_EQUAL_INT(1, Counter_value(counterRef)); } static void testSetValue(void) { Counter_setValue(counterRef,1); TEST_ASSERT_EQUAL_INT(1, Counter_value(counterRef)); Counter_setValue(counterRef,-1); TEST_ASSERT_EQUAL_INT(-1, Counter_value(counterRef)); }