zoukankan      html  css  js  c++  java
  • C语言单元测试框架--EmbedUnit

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

    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));
    }
  • 相关阅读:
    Error: [WinError 10013] 以一种访问权限不允许的方式做了一个访问套接字的尝试。
    react.js
    IE8 frameset SESSION丢失
    回调函数参数问题,闭包解决方案示例
    自定义 httpHandler 配置
    查看SQLServer数据库每个表占用的空间大小
    数据库备份到局域网其他电脑
    SpringSecurity权限管理系统实战—一、项目简介和开发环境准备
    java开发环境配置,看这一篇就足够了!
    java23种设计模式——八、组合模式
  • 原文地址:https://www.cnblogs.com/pingwen/p/9222024.html
Copyright © 2011-2022 走看看