首页 > 范文大全 > 正文

基于cunit的自动测试框架

开篇:润墨网以专业的文秘视角,为您筛选了一篇基于cunit的自动测试框架范文,如需获取更多写作素材,在线客服老师一对一协助。欢迎您的阅读与分享!

摘要:随着现代软件工程的发展,软件质量的要求逐渐得到提高,而软件测试也因此受到越来越多的重视。在企业级应用领域,以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浏览器用户请先下载安装 原版全文