一个通用的单元测试框架的思考和设计.docx
- 文档编号:9504340
- 上传时间:2023-02-05
- 格式:DOCX
- 页数:18
- 大小:201.53KB
一个通用的单元测试框架的思考和设计.docx
《一个通用的单元测试框架的思考和设计.docx》由会员分享,可在线阅读,更多相关《一个通用的单元测试框架的思考和设计.docx(18页珍藏版)》请在冰豆网上搜索。
一个通用的单元测试框架的思考和设计
一个通用的单元测试框架的思考和设计01-思考篇
发布时间:
2011-8-0213:
08作者:
CrazyCoder2010来源:
51Testing软件测试网采编
字体:
小中大|上一篇下一篇|打印|我要投稿|推荐标签:
软件测试单元测试
1.先从问题说起
写过程序的同学都知道,做好单元测试提高代码覆盖率对整个项目意味着什么,但是做好单元测试并不是一件那么简单的事情,因为实际业务逻辑和运行环境的复杂性,导致了我们的单元测试代码不可能都像那些helloWord那么简单,比如现在的业务系统绝大多数都是基于数据库的,怎么做单元测试才能做到每次做单元测试时都是一个干净的测试环境-即上次单元测试的数据库操作不会影响本次测试的结果(比如一个createUser操作,第一次单元测试运行成功了,但是第二次运行却失败了,因为代码里做了重名判断),还有web层的单元测试如何进行,web框架如何启动,那些万恶的httpServletRequest,HttpSession等接口,头大。
。
。
2.现有的框架及对比
目前有很多的开源测试框架,一下列举几个最常用的
junit这个地球人都知道,单元测试的先祖框架,核心,掌握他并熟练运用那是必须滴
testNG,号称下一代单元测试框架,火了一阵子,功能比junit更强大,但是依旧无法撼动junit的王者地位
dbunit,基于数据库的测试框架,提供了可以通过xml格式来准备数据的方案,使测试数据和真实的数据库数据分离开,测试者可以针对自己在文件中造的数据进行程序的断言,也提供了一个与junit集成的基类DBTest,这个基类封装了一些dbunit启动的操作,把一些通用复杂的操作对操作者透明,但是这也是一个劣势,就是一旦继承了这个基类,你就无法继承自己的类(都是java单继承惹的祸)
SpringTest框架,spring2。
5以后提供了一个test框架,提供了大量的基类,测试框架会自动启动spring容器,把spring中的bean自动注入进来,另外还提供了可以对测试方法进行事务控制,每个测试方法测试完成后通过框架控制自动回滚的功能,优势很明显,充分发挥了spring框架的优势,对开发者屏蔽细节,自动事务控制,缺点也很明显,测试必须依赖于spring框架,对于那些没用到spring框架的项目来说只能望洋兴叹了
Struts2Test,struts2提供的单元测试用例基类,提供对httprequest,response,session等对象mock(基于springtest框架),可以初始化struts框架,这对基于struts2的应用系统来说是个福音,不幸的是要用这个框架,必须依赖spring框架,否则一切玩完
3.框架设计的目标
核心的框架都在上面了,不难看出,在这几个主流框架的使用中,spring框架在其中扮演了很重要的角色,如果项目中没有用到spring,有些测试框架的支持功能根本无法使用!
如何让各个框架可以很容易的整合起来又能发挥自身最大的优势呢?
在进行具体的设计之前,我们来展望一下我们期望的测试流程是怎么样的(图)
测试开始前记录数据库状态是1,开始测试后把属于某个测试用例的准备数据加载到数据库中,这时候数据库的状态已经变成了2,接着运行具体的测试用例,测试方法中有对数据库的操作,因此变成了状态3,我们希望对测试的方法进行事务控制,这样在该方法内做的所有数据库操作都可以在测试完成后被会被回滚到先前状态,因此测试方法执行完后,状态又变回了2,当该测试用例的测试方法都执行完毕后,框架负责把先前插入的准备数据删除,这样数据库状态又回到了原始的状态1,这样的优势非常明显,无论这个测试用例执行多少次,用例执行完毕后测试后都会回到原始状态,不会对现有数据造成污染又能保证每次执行都用相同的数据,何乐不为?
刚刚谈了理想,接下来看一下怎样的一个框架才能满足我们的需求
1)不需要和任何框架耦合在一起
这点很重要,整个框架应该围绕一个基本测试框架展看如testng或junit而不是spring这种重量级的框架(相对junit来说spring已经是很重量级的东东了)
2)可以很容易为框架添加新的特性,但是不需要改动框架本身的代码
--对于springtest或dbunit等框架,相对于我们的测试框架只是一些功能上的enhancement,可以通过功能扩展加到框架中,而不是通过继承他们提供基类的方式来直接依赖他们
3)准备测试数据
做基于db的单元测试的人都有感触,单元测试很大一部分时间都在准备数据,因此框架应该支持让使用者通过更加便利的方式去准备数据如常用的excel文件或xml文件或其他csv格式
框架使用者不需要去手动的加载这些数据准备文件,框架需要提供一种契约,如文件名和类名名称路径保持一致,框架自动为当前测试类加载对应的数据,框架使用者只需要书写测试代码,把测试数据放到指定文件夹下即可
4)事务控制
这个没什么捷径,只能靠jdbc的api来实现。
一个通用的单元测试框架的思考和设计02-设计篇
发布时间:
2011-8-0310:
59作者:
CrazyCoder2010来源:
51Testing软件测试网采编
字体:
小中大|上一篇下一篇|打印|我要投稿|推荐标签:
软件测试单元测试
第一节里介绍了我们框架设计的目标,这篇主要介绍的是这个框架主要的设计思路和关键技术点
1.如何扩展junit的功能,使junit在启动时可以做一些我们定制化的功能?
junit4建立了以Runner为核心的测试框架运行机制,在junit3的版本中,我们知道要运行一个junit测试用例,必须继承一个TestCase基类,junit4则不需要这个限制,只需要标注一下要运行测试的方法为@Test就可以了,怎么做到的呢?
就是这个Runner机制,这里不介绍Junit4的运行机制,可以从org。
junit。
runner。
BlockJUnit4ClassRunner中得到答案,像springtest框架也是扩展了这个类的功能来达到扩展目的的
2.如何让junit4框架提供更多的自定义注解的功能?
junit4提供了诸如@Test,@BeforeClass,@Before等注解来简化单元测试过程,在我们这个通用框架的设计中,我们系统提供更多自己定制的注解来扩展junit的功能,如我们希望提供一个@DataSet注解,当测试类有此注解时,框架自动解释这个注解,并把当前类同级目录下加载与类名相同的xsl文件,该文件里存放的是该单元测试类的准备数据,这样就能解决上一节中提到的测试文件和测试类之间约束的目的,开发只需要关注准备数据和写测试类,其他的事情都交给框架去搞吧,通过跟踪Junit的源码我们不难看出单元测试类的执行要经过ObjectcreateTest()方法(测试框架加载测试类)和StatementmethodInvoker(FrameworkMethodmethod,Objecttest)(执行测试方法),这样我们就可以通过扩展BlockJUnit4ClassRunner类通过覆写这两个方法,让其支持我们更多的功能特性
3.回顾一个测试用例的测试过程
一个测试用例的执行大约可以包含这样几个步骤,在创建测试实例后,测试方法前,测试方法执行后,测试方法抛出异常后,因此我们可以根据这些功能定义一个统一接口IUnitTestExecuteListener,接口里定义这4个方法
prepareTestInstance--创建测试实例后
beforeTest--测试方法前
afterTest-测试方法执行后
afterThrowable-测试方法抛出异常后
这样对于不同的功能扩展,我们只需要提供相应的子类即可,如我们前面提到的那个@DataSet注解的方式来加载测试数据准备文件,就可以提供一个ExcelDataProviderListener类,只要在其prepareTestInstance方法里把测试文件内容读出来,插入到数据库中即可-soeasy!
,对于要进行事务控制的测试方法@Transactional标签,我们也可以提供一个实现类来实现事务控制的目的--这样对于框架而言,新功能的扩展只需要添加对应的子类即可,体现了软件设计的‘开-闭’原则
4.框架执行流程图
解释:
IUnit是我们为这个通用框架YY的名字:
)
从这张流程图上可以看出,IunitRunner和IUnitTestExecutionListener是我们整个框架的核心,一个Runner有多个Listener,当测试的生命周期开始后,runner会循环调用已注册listener的prepareTestInstance,beforeTest,afterTest,afterThrowable方法执行对应的功能
5.详细设计-类图
解释:
整个框架最核心的类和接口只有三个IunitRunner,这个是运行的切入点,用来注册每个测试类指定的listener(通过寻找测试中上的@IUnitTestExecuteListeners标注),所有的功能扩展都是围绕IUnitTestExecuteListener展开,如类图中描述的GuiceStrapupListener用来启动guice容器,DataProviderListener用来加载测试准备数据
6.千呼万唤始出来-最终的测试用例长什么样子
对于最终使用框架的开发者而言,根据自身需要通过注解来动态加载所需要的listener即可(可以指定多个),注意测试用例上要加个@RunWith标注,指定要执行的runner为IunitRunner这样junit框架才能用我们提供的runner来运行,实际使用的时候这些东东可以都放到一个测试父类中去完成,开发者只需要关注自身用到的listener即可
viewplaincopytoclipboardprint?
packagecom.crazycoder2010.iunit;
importstaticorg.hamcrest.MatcherAssert.assertThat;
importstaticorg.hamcrest.Matchers.equalTo;
importorg.junit.Test;
importorg.junit.runner.RunWith;
importcom.crazycoder2010.iunit.annotation.IUnitDataSet;
importcom.crazycoder2010.iunit.annotation.IUnitTestExecuteListeners;
@IUnitDataSet(dbunitFile="AppTest.xml")
@RunWith(IUnitRunner.class)
@IUnitTestExecuteListeners({TransactionalListener.class,DatasetProviderListener.class})
publicclassAppTestextendsAbstractIUnitTestCase{
@Test
publicvoidtestHello(){
assertThat("hello",equalTo("hello"));
}
}
一个通用的单元测试框架的思考和设计03-实现篇-核心类源码
发布时间:
2011-8-0410:
37作者:
CrazyCoder2010来源:
51Testing软件测试网采编
字体:
小中大|上一篇下一篇|打印|我要投稿|推荐标签:
软件测试单元测试
第二节里我们介绍了iunit整体的设计思路以及核心类之间的关系,这篇将以源码+解释的方式来演示核心类的实现方式
1.IUnitRunner类
这个类是测试的入口类,直接继承自junit4。
8的BlockJunit4ClassRunner,在构造函数里,我们把iunit框架的扩展功能添加了进来,因为整个框架呃设计都是基于Listener的,所以只需要把监听器在框架运行的时候加载进来即可--见构造函数,listener的注册是通过注解来进行的,因为测试类本身可能会有继承关系,因此需要遍历父类中的Listener,把子类+所有父类中的Listener合并起来,当然还要注意剔除掉重复注册的Listener,否则很可能导致一个Listener被执行多次(既在子类中注册过了又在父类中注册过了)
packagecom.crazycoder2010.iunit;
importjava.util.ArrayList;
importjava.util.List;
importorg.junit.runners.BlockJUnit4ClassRunner;
importorg.junit.runners.model.FrameworkMethod;
importorg.junit.runners.model.InitializationError;
importorg.junit.runners.model.Statement;
importcom.crazycoder2010.iunit.annotation.IUnitTestExecuteListeners;
publicclassIUnitRunnerextendsBlockJUnit4ClassRunner{
/**
*监听器
*/
privateList
privateClass<?
>clazz;
privateTestContexttestContext;
publicIUnitRunner(Class<?
>klass)throwsInitializationError{
super(klass);
//这个构造函数是junt的调用入口,这里我们把扩展功能的初始化写到其后
this.clazz=klass;
this.testContext=newTestContext();
initListeners();
}
privatevoidinitListeners(){
this.executeListeners.addAll(findListeners());
}
/**
*解析为当前测试类注册的监听器
*@return
*/
@SuppressWarnings(“rawtypes”)
privateList
List
List
Class<?
>c=this.clazz;
while(c!
=null){
IUnitTestExecuteListenerslistener=c.getAnnotation(IUnitTestExecuteListeners.class);
if(listener!
=null){
for(Class<?
extendsIUnitTestExecuteListener>l:
listener.value()){
if(!
listeners.contains(l)){//去重
listeners.add(l);
}
}
}
c=c.getSuperclass();//查找父类中的监听器
}
for(Classclazz:
listeners){
try{
result.add((IUnitTestExecuteListener)clazz.newInstance());//通过反射将Listener实例加入到监听列表中
}catch(InstantiationExceptione){
e.printStackTrace();
}catch(IllegalAccessExceptione){
e.printStackTrace();
}
}
returnresult;
}
@Override
protectedObjectcreateTest()throwsException{
ObjecttestInstance=super.createTest();
//加上我们框架的扩展功能
this.testContext.setTestInstance(testInstance);
for(IUnitTestExecuteListenerexecuteListener:
this.executeListeners){
executeListener.prepareTestInstance(testContext);
}
returntestInstance;
}
@Override
protectedStatementmethodInvoker(FrameworkMethodmethod,Objecttest){
for(IUnitTestExecuteListenerexecuteListener:
this.executeListeners){
try{
executeListener.beforeTest(testContext);
}catch(Exceptione){
e.printStackTrace();
}
}
Statementstatement=null;
for(IUnitTestExecuteListenerexecuteListener:
this.executeListeners){
try{
statement=super.methodInvoker(method,test);
executeListener.afterTest(testContext);
}catch(Exceptione){
testContext.setThrowable(e);
try{
executeListener.afterThrowable(testContext);
}catch(Exceptione1){
e1.printStackTrace();
}
}
}
returnstatement;
}
}
2、IUnitTestExecuteListener接口
这个接口定义了测试用例执行生命周期的几个关键点
packagecom.crazycoder2010.iunit.annotation;
importjava.lang.annotation.Documented;
importjava.lang.annotation.ElementType;
importjava.lang.annotation.Inherited;
importjava.lang.annotation.Retention;
importjava.lang.annotation.RetentionPolicy;
importjava.lang.annotation.Target;
importcom.crazycoder2010.iunit.IUnitTestExecuteListener;
/**
*为TestCase注册监听器
*
*@authorKevin
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
@Documented
public@interfaceIUnitTestExecuteListeners{
/**
*实际用到的监听器类
*
*@return
*/
Class<?
extendsIUnitTestExecuteListener>[]value()default{};
}
3.AbstractIUnitTestCase测试基类
这个类是为了便于测试定义了一个TestCase的基类,可以在此注册一些通用的监听器,注意@Runwith(IUnitRunner.class)这个是关键,否则我们写在runner中扩展的功能是不会被junit4执行到的
packagecom.crazycoder2010.iunit;
importorg.junit.runner.RunWith;
importcom.crazycoder2010.iunit.annotation.IUnitTestExecuteListeners;
@RunWith(IUnitRunner.class)
@IUnitTestExecuteListeners({DatasetProviderListener.class})
publicclassAbstractIUnitTestCase{
}
一个通用的单元测试框架的思考和设计04-实现篇-测试数据自动插入删除
发布时间:
2011-8-0510:
50作者:
CrazyCoder2010来源:
51Testing软件测试网采编
字体:
小中大|上一篇下一篇|打印|我要投稿|推荐标签:
软件测试单元测试
上篇文章罗列了整个框架的核心接口,这篇文章将提供IunitExecutionListener的一个实现类-DatasetProviderListener,主要用来将测试数据插入到数据库中,待测试完成后自动删除数据
1.dbunit为核心的db测试
dbunit是个很好的数据库测试框架,提供了多种准备数据的操作策略来简化测试数据的插入或更新操作,参考http:
//www.dbunit.org/components.html#deleteall
操作
描述
DatabaseOperation.UPDATE
使用准备数据更新现有db中的数据(根据准备数据主键ID值),如果数据库中不存在准备数据中的ID,报错退出
DatabaseOperation.INSERT
将测试数据插入到数据库中,如果数据库中已经有对应id的数据,则报错退
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 一个 通用 单元测试 框架 思考 设计