c語言單元測試框架--CuTest

一、簡介

CuTest是一款微小的C語言單元測試框,是我迄今爲止見到的最簡潔的測試框架之一,只有2個文件,CuTest.c和CuTest.h,所有代碼加起來不到一千行。麻雀雖小,五臟俱全,測試的構建、測試的管理、測試語句,都所有包含在內。linux

二、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。
待測函數原型: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());

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

三、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);
}
相關文章
相關標籤/搜索