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("\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,\
- }
其结构体定义为:
- 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));
- }
来源: https://www.cnblogs.com/pingwen/p/9222024.html