一、Embedded Unit 简介
Embedded Unit(简称embUnit)是一个针对嵌入式C系统的单元测试框架。它不依赖于标准的C函数库,所有的对象都被静态编译链接。因此,可以比较方便地将其移植到嵌入式平台。
下载地址:http://sourceforge.net/projects/embunit/files/
【备注】:Embedded Unit测试原理是通过将预期值与实际值进行比较来测试函数的逻辑,只能实现函数级别的单元测试而已,呵呵。
二、目标平台简介
硬件平台:PowerPC
操作系统:基于uclinux内核,但是所有系统调用都自己实现的一个精简操作系统;
三、移植思路
- 由于embUnit不依赖于标准的C函数库,因此,将我们的编译选项添加到embUnit中的Makefile中,将其代码编译成一个静态库,然后链接到我们原有的程序中;
- 额外创建一个源文件,用于编写测试代码,该文件也通过编译、链接,将其与原有的程序链接在一起;
- 在原有程序的main中(即程序入口处),调用embUnit这个框架提供的API函数,执行对函数的单元测试;
四、实战步骤
1. 下载的embUnit的源代码解压后,在解压形成的目录里面有一个embunit目录,这个目录就是embUnit的源代码所在的目录,首先我们分析这个目录下的makefile文件,文件内容如下:
CC = gcc CFLAGS = -O AR = ar ARFLAGS = ru RANLIB = ranlib RM = rm OUTPUT = ../lib/ TARGET = libembUnit.a OBJS = AssertImpl.o RepeatedTest.o stdImpl.o TestCaller.o TestCase.o TestResult.o TestRunner.o TestSuite.o all: $(TARGET) $(TARGET): $(OBJS) $(AR) $(ARFLAGS) $(OUTPUT)$@ $(OBJS) $(RANLIB) $(OUTPUT)$@ .c.o: $(CC) $(CFLAGS) $(INCLUDES) -c $< AssertImpl.o: AssertImpl.h stdImpl.h RepeatedTest.o: RepeatedTest.h Test.h stdImpl.o: stdImpl.h TestCaller.o: TestCaller.h TestResult.h TestListener.h TestCase.h Test.h TestCase.o: TestCase.h TestResult.h TestListener.h Test.h TestResult.o: TestResult.h TestListener.h Test.h TestRunner.o: TestRunner.h TestResult.h TestListener.h Test.h stdImpl.h config.h TestSuite.o: TestSuite.h TestResult.h TestListener.h Test.h clean: -$(RM) $(OBJS) $(TARGET) .PHONY: clean all
分析可知,这个makefile会将当前目录下的源文件编译和链接成一个名为libembUnit.a的静态库文件。然后,我们只要修改相应的编译选项,去掉不用的选项,下面是修改的Makefile:
include ../build/common.mk .c.o: $(CC) $(CFLAGS) -c -o $*.o $< embu_OBJS = AssertImpl.o RepeatedTest.o stdImpl.o TestCaller.o TestCase.o TestResult.o TestRunner.o TestSuite.o embu_DIR = ../object/common/ TARGET = libembUnit.a all: $(TARGET) $(TARGET): $(embu_OBJS) $(AR) $(ARFLAGS) $(TARGET) $(embu_OBJS) cp -f $(TARGET) $(embu_DIR) .c.o: $(CC) $(CFLAGS) $(INCLUDES) -c $< AssertImpl.o: AssertImpl.h stdImpl.h RepeatedTest.o: RepeatedTest.h Test.h stdImpl.o: stdImpl.h TestCaller.o: TestCaller.h TestResult.h TestListener.h TestCase.h Test.h TestCase.o: TestCase.h TestResult.h TestListener.h Test.h TestResult.o: TestResult.h TestListener.h Test.h TestRunner.o: TestRunner.h TestResult.h TestListener.h Test.h stdImpl.h config.h TestSuite.o: TestSuite.h TestResult.h TestListener.h Test.h clean: -rm -f *.o *.a .PHONY: clean all
仔细分析,修改的内容就是将编译选项修改了,然后将生成的libembUnit.a文件复制到一个公共目录下;
【备注】其中../build/common.mk文件是我们源程序中公共选项脚本文件,里面定义了编译和链接的一些选项而已,呵呵
2. 修改完Makefile后,如果编译,会提示stdio.h这个头文件找不到。原因是embUnit的源代码中的config.h这个文件include了<stdio.h>这个C库文件,而由于我们的操作系统完全自己实现,并且没有提供stdio.h这个头文件,因此,将其注释掉即可;然后再执行make,就可以编译通过了,虽然会有一些警告,不过可以忽略;
3. 书写测试代码
/* include local files */ /* include embUnit include */ #include "../embUnit/embUnit.h" #include "../embUnit/AssertImpl.h" #include "../embUnit/config.h" #include "../embUnit/HelperMacro.h" #include "../embUnit/RepeatedTest.h" #include "../embUnit/stdImpl.h" #include "../embUnit/Test.h" #include "../embUnit/TestCaller.h" #include "../embUnit/TestCase.h" #include "../embUnit/TestListener.h" #include "../embUnit/TestResult.h" #include "../embUnit/TestRunner.h" #include "../embUnit/TestSuite.h" #include "../rts_include/test.h" int iFile = -1; static void setUp(void) { /* initialize */ iFile = open("testFile.txt", O_WRONLY|O_CREAT|O_APPEND|O_NAND); } static void tearDown(void) { /* terminate */ close(iFile); } static void testFile(void) { char buff[4] = "abcd"; TEST_ASSERT_EQUAL_INT(6, write(iFile, buff, 4)); } /*embunit:impl=+ */ /*embunit:impl=- */ TestRef testFile_tests(void) { EMB_UNIT_TESTFIXTURES(fixtures) { /*embunit:fixtures=+ */ /*embunit:fixtures=- */ new_TestFixture("testFile", testFile), }; EMB_UNIT_TESTCALLER(test,"test",setUp,tearDown,fixtures); return (TestRef)&test; };
首先注意测试代码一方面要引用embUnit的头文件,另一方面也要引用原有程序相应的头文件,这样才能既使用embUnit提供的API函数,又能使用原有程序提供的接口函数;
上述代码的解释如下:
- 上述代码的目标是对write函数进行测试,方法就是先open一个文件,然后写入固定的字节,判断写入的字节数是否正确,最后关闭文件。
- setUp函数的作用是:提供待测试函数的前端输入。譬如,上述代码要测试write函数,必须要先用open打开一个文件,那么就可以在setUp这个函数中调用open函数去创建要write的文件;
- tearDown函数的作用是:提供待测试函数的后端处理。譬如,上述代码,写入文件后,要关闭文件,那么就可以在tearDown这个函数调用close函数关闭文件;
- testFile函数(函数名其实可以自己定义)的作用是:提供测试逻辑。譬如,上述代码,调用embUnit提供的断言宏,进行判断write函数是否执行成功。
- testFile_tests函数的作用是:将上述几个函数整合到一个测试suite中,以供后续程序调用;
对测试代码编译(仍然采用目标平台的编译选项进行编译),使其生成的.a(如test.a)文件,并将.a文件复制到公共的库文件目录下;
4. 在原有函数的入口出执行测试程序
代码如下:
#include "../embUnit/embUnit.h" #include "../embUnit/AssertImpl.h" #include "../embUnit/config.h" #include "../embUnit/HelperMacro.h" #include "../embUnit/RepeatedTest.h" #include "../embUnit/stdImpl.h" #include "../embUnit/Test.h" #include "../embUnit/TestCaller.h" #include "../embUnit/TestCase.h" #include "../embUnit/TestListener.h" #include "../embUnit/TestResult.h" #include "../embUnit/TestRunner.h" #include "../embUnit/TestSuite.h" void main(void) { printf("***********************************************************\n"); printf("************** Unit Test Start ***************\n"); printf("***********************************************************\n"); /* 将测试结果按照编译格式输出 */ TestRunner_start(); TestRunner_runTest(testFile_tests()); TestRunner_end(); printf("***********************************************************\n"); printf("************** Unit Test End ***************\n"); printf("***********************************************************\n"); }
在上述代码中,可以看到语句TestRunner_runTest(testFile_tests());调用的就是在测试代码中声明的testFile_tests函数;
需要的注意的是,在该程序中也要引用embUnit提供的头文件,否则无法编译通过。
5. 将上述代码也编译成.a文件,并且与刚才编译生成的libembUnit.a和test.a文件,以及原有程序的其他.a文件链接一个可执行文件,并将其烧录到目标机器中,然后即可看到运行结果。下图是本人的运行结果:
由于写入的是4个字节,但是测试代码的预期值是6个字节,因此,测试没有通过。
到此,整个移植过程结束。不过,上述测试结果显示,不是那么友好,很幸运的是embUnit提供的源码中还有一个格式化测试结果的工具的源代码,我们就照葫芦画瓢将这个工具也移植过来;
五、移植embUnit的格式化测试结果工具
在刚才下载的压缩包解压后的根目录下有个textui的目录,这个目录提供的就是格式化测试结果的工具的源代码,移植的方法与上面类似,现简介如下:
1. 修改makefile
textui原有的makefile如下所示:
CC = gcc CFLAGS = -O INCLUDES = .. LIBS = ../lib AR = ar ARFLAGS = ru RANLIB = ranlib RM = rm OUTPUT = ../lib/ TARGET = libtextui.a OBJS = TextUIRunner.o XMLOutputter.o TextOutputter.o CompilerOutputter.o all: $(TARGET) $(TARGET): $(OBJS) $(AR) $(ARFLAGS) $(OUTPUT)$@ $(OBJS) $(RANLIB) $(OUTPUT)$@ .c.o: $(CC) $(CFLAGS) -I$(INCLUDES) -c $< TextUIRunner.o: TextUIRunner.h XMLOutputter.h TextOutputter.h CompilerOutputter.h Outputter.h XMLOutputter.o: XMLOutputter.h Outputter.h TextOutputter.o: TextOutputter.h Outputter.h CompilerOutputter.o: CompilerOutputter.h Outputter.h clean: -$(RM) $(TARGET) $(OBJS) .PHONY: clean all
很明显,原有的makefile是将所有的源文件编译生成一个libtextui.a的文件,现将这个makefile修改如下:
与前述一样,只是修改的编译选项,并且将生成的libtextui.a文件复制到一个公共的目录下;
2. 编译textui
修改完Makefile文件后,此时编译会提示:stdout未定义;
原因是:textui中使用了fprintf将信息输出的stdout(即标准输出,一般指屏幕),但是我们的目标机不支持显示器等标准输出,也没有对stdout进行定义。
解决办法是:在Outputter.h文件中,重新定义printf,即,忽略stdout,使用printf代替fprintf,具体如下所示:
#define fprintf(stdout, formats, args...) printf(formats, ##args)
解决上述问题后,编译时,仍然会提示错误,主要是头文件找不到的错误;
原因是:在textui的源文件中,需要引用embUnit的头文件,但是其采用的是#include <>方式,当然找不到对应的头文件了;
解决办法:我们将这些头文件的引用方式改为#include " "方式,并且指明对应头文件的相对路径;
3. 修改在程序入口处的调用测试程序的代码,具体如下所示:
void main(void) { printf("***********************************************************\n"); printf("************** Unit Test Start ***************\n"); printf("***********************************************************\n"); /* 将测试结果按照编译格式输出 */ TestRunner_start(); TestRunner_runTest(testFile_tests()); TestRunner_end(); /* 将测试结果按文本格式输出 */ TextUIRunner_setOutputter(TextOutputter_outputter()); TextUIRunner_start(); TextUIRunner_runTest(testFile_tests()); TextUIRunner_end(); /* 将测试结果按照XML格式输出 */ TextUIRunner_setOutputter(XMLOutputter_outputter()); TextUIRunner_start(); TextUIRunner_runTest(testFile_tests()); TextUIRunner_end(); printf("***********************************************************\n"); printf("************** Unit Test End ***************\n"); printf("***********************************************************\n"); }
从上述代码可知,注册了两种测试结果输出方式:TextUIRunner_setOutputter(TextOutputter_outputter()); TextUIRunner_setOutputter(XMLOutputter_outputter());
4. 全编译所有代码,并链接成可执行程序,烧到目标机器上,即可看到测试结果,下图就是测试结果:
到此,整个移植过程结束。
六、经验总结
- 不同平台间的代码移植原理其实很简单,就是将待移植的代码使用目标平台的编译环境进行编译,然后将其链接到原有的程序中;
- 待移植的代码一般会使用#include<>方式引用头文件,因此,移植的时候需要修改待移植程序的源文件的关于头文件引用方式;
- 待移植的代码还可能会使用一些目标平台不支持的API函数或这系统调用,因此,移植的时候应该重新封装这些接口;
- 尽量将待移植的代码独立成一个单独的模块,以静态库的方式链接到原有程序中,以便在不需要这些东东的时候,只要不链接其静态库即可;
参考资料:
[1].http://embunit.sourceforge.net/
转载:http://www.cnblogs.com/cnpirate/archive/2012/09/27/2704548.html