摘要:随着现代软件工程的发展,软件质量的要求逐渐得到提高,而软件测试也因此受到越来越多的重视。在企业级应用领域,以xunit为代表的自动测试框架已经趋于成熟。但是在嵌入式系统开发领域,由于软件系统对硬件平台的依赖,软件在通用性和易测试性方面都比较欠缺,从而导致自动测试系统的贫乏。本文分析说明了软件测试的作用,特别是在嵌入式开发过程中的作用,以及实施软件测试所需要的代价。基于以上理论,本文论述了一个基于cunit设计的自动测试框架。鉴于自动测试系统对测试工作的重要性,模仿xunit的特性,对cunit进行了改进。并且针对嵌入式系统的特性,在框架中加入了守护线程,用以模拟中断等外部事件。
关键词:cunit; 测试框架;自动测试;测试包;多线程支持
中***分类号:TP311文献标识码:A文章编号:1009-3044(2007)18-31631-03
Unit Test Framework Based on Cunit
LIU Bo
(College of Software Engineering,Southeast University,Nanjing 210096,China)
Abstract:unit test framework like xunit is widely used in enterprise application, but seldom in embedded system because different hardware architectures have different requirements. This paper demonstrated a unit test framework based on cunit, which could be used to test a large part of a embedded system, and discussed how to use it to improve the quality of the software while keep the cost low.
Key words:cunit;unit test framework;test suite; multi-threaded
1 引言――测试与自动测试系统
随着现代软件工程的发展,软件测试做为软件质量保证的最重要手段之一,越来越受到重视,如何有效迅捷的进行测试,也成为了业界讨论的重点。
在企业级应用(enterprise application)领域,以xunit为代表的自动测试技术已经趋于成熟。实践证明,自动测试技术,特别是采用了测试驱动开发模式的自动测试技术,可以很大程度的改善软件质量,而且实施的代价也是可接受的。
但是在嵌入式系统开发领域,实施自动测试的代价相对来说就大了很多。由于软件系统对硬件平台非常依赖,测试通常都是在真机或者是模拟器上进行,而这种测试即为手动测试,效率是相当低下的。而如果采用做桩的模式对系统进行自动测试,那么创建桩的工作量又非常的大,其代价是不可以接受的。
因此,现存的针对嵌入式系统,或者针对c的自动测试框架几乎没有。但是我们认为,正是由于嵌入式系统测试很难实施,单元测试就更加重要。而对于手动测试和自动测试,可以进行动态的选择。首先,软件系统的设计必须是松耦合的模块化设计。之后,根据各个模块的特性进行分析――对硬件平台依赖比较大的,进行手动测试;对硬件平台依赖比较小而自身逻辑比较复杂的,做桩进行自动测试。
所以,我们需要一个简单但是完善的自动测试框架系统。通过调研,我们最终决定采用开源系统cunit,将其扩充改进成我们所需的框架。
2 Cunit及其工作原理
cunit是一个开源软件,基于zlib/libp许可证, 作者是Asim Jalis。可以在sourceforge上找到该项目。本文采用的是1.4版本。
该版本的cunit的运行环境是linux,因为其利用了linux的bash的一些功能。
cunit的工作原理是,利用bash的grep功能,搜索当前目录下所有.c文件中的void Test***(CuTest *tc){...} 形式的函数,其中***表示可以用任意字符替代。然后,将所有这些测试函数集中起来,生成一个带有main的.c文件。对所有文件进行编译链接后运行,即会逐步调用所有的void Test***(CuTest *tc){...} 函数,完成自动测试。
测试结果默认会输出到控制台上,当然还可以用bash的重定向功能输出到指定文件中去。
cunit提供了一批类xunit的assert函数。函数原型为void FunctionName(CuTest* tc,…)。其中,FunctionName是函数名称,在其头文件中有定义;tc是一个框架需要的指针,其参数值就是Test函数中的参数tc。在实际使用中只需要直接赋值就可以,框架会使用它的。
这些assert函数的作用是,断言当前条件是否为真。断言方式由函数功能定义,如比较两个字符串是否相等,两个整数是否相等之类。
一旦断言为真,则继续执行。当某个Test函数成功执行完毕,则算通过了一个test。
一旦某个断言为假,则直接跳出这个Test函数(利用longjmp功能),并记录一个test fail。
该断言机制与xunit是一致的。
3 基于cunit的进一步设计
cunit实现了最基本的单元自动测试功能,并且封装了比较好的字符串处理功能以提供友好的用户界面(尽管只是字符界面)。但是,相对于我们的需求,他还是有一些欠缺的地方。下面就对cunit进行一些改进,以实现xunit具有的一些特性。
3.1Startup 和 Teardown
每一个Test函数就是一个测试用例。在xunit中,有StartUp和TearDown功能,用来在每个测试用例测试之前建立环境,测试完毕之后清除环境。我们在cunit上做改进以实现这种功能。
首先,打开cunit的头文件CuTest.h,找到结构体CuSuite的定义。我们在之前定义两个函数指针类型:
typedef void (*StartUpFunc)();
typedef void (*TearDownFunc)();
接下来在CuSuite的定义中,添加两个函数指针变量,分边指向用户定义的StartUp和TearDown函数(斜体为新添加部分):
typedef struct
{int count;
CuTest* list[MAX_TEST_CASES];
int failCount;
StartUpFunc startUp;
TearDownFunc tearDown;
} CuSuite;
再打开CuTest.c,找到函数CuSuiteRun的定义,做如下更改(斜体为新添加部分):
void CuSuiteRun(CuSuite* testSuite)
{int i;
for (i = 0 ; i < testSuite->count ; ++i)
{/*在每个测试用例测试前,建立环境*/
本文为全文原貌 未安装PDF浏览器用户请先***安装 原版全文
if(testSuite->startUp != NULL)
{(testSuite->startUp)();
}
CuTest* testCase = testSuite->list[i];
CuTestRun(testCase);
if (testCase->failed)
{
testSuite->failCount += 1;
}
/*在每个测试用例测试结束后,清除环境*/
If (testSuite->tearDown != NULL)
{
(testSuite->tearDown)();
}
}
}
这样,在每个测试用例运行的前后,都会自动运行StartUp和TearDown了。
当然,由于StartUp与TearDown都是由用户提供,我们还需要对自动生成脚本make-tests.sh做修改:
添加如下的外部函数声明,注意声明的类型需与CuTest.h中定义的两个函数指针一致:
extern void StartUp();
extern void TearDown();
这样声明后,链接时链接器会去寻找这两个函数的定义。如果用户未提供定义,则链接无法通过。
再在RunAllTests函数中,对CuSuite结构suite的两个函数指针赋值(斜体为新添加部分):
void RunAllTests(void)
{ CuString *output = CuStringNew();
CuSuite* suite = CuSuiteNew();
suite->startUp = StartUp;
suite->tearDown = TearDown;
……
注意,该函数存在于make-tests.sh中,在创建测试包的时候被自动生成。
这样一来,StartUp和TearDown功能就被添加到cunit框架中去了。
值得注意的是,StartUp和TearDown功能是添加到测试包中的,而非添加给测试用例。测试包的概念是一组相似的测试用例,而且这组测试用例具有相同的运行环境。
3.2与项目的整合
cunit所建立的测试系统是测试包,但是我们所需要的实际上是若干个测试包,以对应软件系统中不同的软件模块。这里,我们不再修改cunit,而是利用软件系统的文件结构和linux的bash所提供的一些功能,自动调用每个模块所对应的测试包,再将测试结果自动整合。
如***1所示,我们通过树形文件结构将cunit以及所有的测试用例与项目源码整合在了一起。在一个硬件芯片上,通常会有15%的元件做为测试单元存在。软件系统也是一样,测试用例是属于源代码的一部分存在于系统之中的。
***1 整合了测试的项目构架
Src下是项目的源码,其下分成了若干模块。
Inc是项目源码对应的头文件。
Test下是所有的测试用例。如上***所示,测试用例与项目源码具有一一对应的关系,结构上也同样对应。
Util存放了一些工具,如cunit等。(是的,cunit和测试代码可以分开的,因为我们有make)
首先,如***1,在合适的位置建立测试包。每个测试包包含其自身的测试用例,测试用例需要用到的桩,以及一个make文件去定义测试所需要包含的项目源文件以及工具文件。
而后,在Test根下建立一个bash脚本,遍历所有的测试包,对每个包进行编译?链接?测试,并将测试结果放置在测试包下的一个记录文件中。
最后,从所有的测试包中取出记录的测试信息,整合以后提供给用户。
如此一来,一个自动测试框架就建立好了,可以方便的执行测试先行开发。如果不需要每次都对所有的测试包进行测试,简单修改bash脚本就可以了。
4 高级功能:多线程的支持
嵌入式系统虽然很少有并发计算的需要,但是由于其与硬件的联系非常紧密,会有另外一种形式的并发发生:中断。
中断当然无法用桩来模拟,因为桩只是提供一些假的接口,返回正确或错误的值(实际上,桩返回错误值更有意义,因为这样可以检验模块内部处理错误情况的正确性,而无需人为的创造错误条件),它不可能主动发起某个动作。
理论上来说,中断也是不可以用多线程来模拟的,因为其工作原理不一样。有很多中断也无法用多线程来模拟,如EEPROM和UART的读写(慢速IO设备,利用中断实现异步读写)。不过,还是有很多情况下,中断事件是可以通过多线程来模拟的,比如某个计时器的触发,某个外部传感器的值更新等。这些情况下中断仅仅是更新某个全局变量,或者是调用某个简单的函数,完全可以用多线程来模拟,并且这种模拟很有价值――与系统的运行逻辑紧密相关。比如,某个逻辑模块需要检测系统上某一传感器的值,当其发生变化时,通过LED或其他方式做出相应的反应,测试该逻辑模块时,就需要一个辅助线程来改变传感器的值。
因此,我们设计了一个StubThread模块,放在系统的util目录下,来提供多线程的支持。多线程的实现基于兼容于posix的pthread。该模块的功能是,创建n个线程,其中n为用户定义的线程数。每个线程在经过某一指定的时间后,调用相应的回调函数(即触发事件)。每个线程都可以被***地开关。
StubThread.h:
#ifndef _STUB_THREAD_H
#define _STUB_THREAD_H
/*回调函数指针*/
typedef void (*CallBackFunc)();
typedef enum{
TRUE = 1,
FALSE = 0
}BOOL;
/*线程数据结构体*/
typedef struct{
BOOL active; /*是否触发事件。*/
double delay; /*每两次事件的时间间隔*/
CallBackFunc callee;/*回调函数指针*/
}STUB_SEM;
extern STUB_SEM * pStubSem;
extern const int StubSemLength;
void StartStubThread();
void StopStubThread();
#endif
使用该工具的客户代码需要提供pStubSem和StubSemLength,前者指向一个STUB_SEM类型的数组,后者标识该数组的长度。
STUB_SEM中,active表示该事件是否会发生,delay表示每两次该事件发生的时间间隔,callee则是当该事件发生时,事件响应函数的指针。
若active被置为true,则事件会被周期性的触发。若只希望该事件被触发一次,则可在响应函数中将active设置为false。
callee指向的相应函数是桩提供的,该响应函数根据实际需要,伪造数据或触发事件。
以下是StubThread模块的实现(zlib/libp并没有要求公开修改后的源码)。
StubThread.c:
#include "StubThread.h"
#include
tspec.省略_sec) * nanoFactor);
nanosleep(&tspec, NULL);
}
/*线程方法。每个线程都运行该方法。方法结束时相应的线程也会结束*/
void ThreadFunction(STUB_SEM *sem){
while(running){
if(sem->active){
stWait(sem->delay);/*等待指定的时间*/
(sem->callee)();/*调用回调函数*/
}
}
}
/*此方法将启动所有辅助线程*/
void StartStubThread(){
int ret,i;
/*start thread:*/
running = TRUE;
for(i=0;i
ret = pthread_create(&(pthreadIDs[i]), NULL, (void*)ThreadFunction,&pStubSem[i]);
if(ret!=0){/*如果发生错误*/
running = FALSE;
printf("Create pthread error!n");
exit(1);
}
}
}
/*此方法将终止所有线程*/
void StopStubThread(){
int i;
/*set semaphore to stop it:*/
running = FALSE;
/*wait for stop:*/
for(i=0;i
pthread_join(pthreadIDs[i],NULL);
/*reset*/
pthreadIDs[i] = 0;
}
}
用StartStubThread后,会为每个STUB_SEM开启一个***的线程。而调用StopStubThread后,会把所有的线程终止掉。
STUB_SEM中的active做为信号量实现线程间通讯。
5 结论
经过实际项目的使用,该自动测试框架运行正常,用较低的代价保证了软件质量,对于逻辑比较复杂的模块是相当有效果的。但是由于创建桩的成本比较高,并且某些中断事件用该框架提供的模型模拟并不可靠,选择哪些模块使用该框架测试是非常重要的。选择的主要依据是逻辑较复杂且对特定的硬件功能(如数模转换,UART等)无依赖。
在实际项目中,我们仅将逻辑层用这套自动测试工具进行了测试。逻辑层之上的应用层与用户交互非常频繁,所需做的桩很多,不合适。而底层与硬件关系非常紧密,逻辑简单,但需要在真机上实测。
最终,项目的测试时间比依据经验估计的时间缩短了近四分之一(包括写测试用例与测试桩的时间,但不包括设计实现本测试框架的时间)。实践证明,嵌入式系统的开发也可以应用自动测试技术。在合理选择测试范围的情况下,软件系统的质量与测试效率可以得到较显著的提高。
参考文献:
[1]H ZHU,PAV HALL,JHR MAY.Software Unit Test Coverage and Adequacy[M].ACM Computing Surveys,1997.
[2]Paul Hamill.Unit Test Frameworks[M].O'reilly,2004.
[3]J.Ramsey,V. Basili.Structural Coverage of Functional Testing [R].Proceedings of the IEEE 8th International Conference on Software Engineering,1985-08.
[4]Kent Beck,Test Driven Development: By Example [M].Addison-Wesley Longman,2002.
[5]Bel Lewis,Daniel J.Berg,Pthreads Primer: A Guide to Multithreaded Programming [M].SunSoft Press,1996.
注:本文中所涉及到的***表、注解、公式等内容请以PDF格式阅读原文。
本文为全文原貌 未安装PDF浏览器用户请先***安装 原版全文
转载请注明出处学文网 » 基于cunit的自动测试框架