查看原文
其他

分享一个c语言单元测试框架

点击上方「嵌入式大杂烩」,选择「置顶公众号」第一时间查看嵌入式笔记!

来源:https://www.cnblogs.com/pingwen/p/9216004.html

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

温馨提示

由于微信公众号近期改变了推送规则,如果您想经常看到我们的文章,可以在每次阅读后,在页面下方点一个「赞」或「在看」,这样每次推送的文章才会第一时间出现在您的订阅列表里。

版权声明:本文来源网络,免费传达知识,版权归原作者所有。如涉及作品版权问题,请联系我进行删除。


猜你喜欢:

干货 | 函数指针与软件设计

干货 | C语言变长数组的经典应用

万字整理,肝翻Linux内存管理所有知识点


在公众号聊天界面回复1024,可获取嵌入式资源;回复 ,可查看文章汇总。

文章都看完了不点个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存