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


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

 


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com