C語言單元測試框架--EmbedUnit

一、簡介

Embedded Unit是個純標準c構建的單元測試框架,主要用在嵌入式c的單體測試上,其主要特色是不依賴於任何C的標準庫,全部的對象都是靜態分配。編程

最先這個項目託管在SourceForge上(https://sourceforge.net/projects/embunit ),目前在GitHub也有多個拷貝。數組

二、框架剖析

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,\
    }

其結構體定義爲:spa

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,被返回傳遞給測試執行函數。.net

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

三、測試實例

下面演示了一個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));
}
相關文章
相關標籤/搜索