首页 > 代码库 > spring--测试--13
spring--测试--13
13.1 概述
13.1.1 测试
软件测试的目的首先是为了保证软件功能的正确性,其次是为了保证软件的质量,软件测试相当复杂,已经超出本书所涉及的范围,本节将只介绍软件测试流程中前两个步骤:单元测试和集成测试。
Spring提供了专门的测试模块用于简化单元测试和集成测试,单元测试和集成测试一般由程序员实现。
13.2 单元测试
13.2.1 概述
单元测试是最细粒度的测试,即具有原子性,通常测试的是某个功能(如测试类中的某个方法的功能)。
采用依赖注入后我们的代码对Spring IoC容器几乎没有任何依赖,因此在对我们代码进行测试时无需依赖Spring IoC容器,我们只需要通过简单的实例化对象、注入依赖然后测试相应方法来测试该方法是否完成我们预期的功能。
在本书中使用的传统开发流程,即先编写代码实现功能,然后再写测试来验证功能是否正确,而不是测试驱动开发,测试驱动开发是指在编写代码实现功能之前先写测试,然后再根据测试来写满足测试要求的功能代码,通过测试来驱动开发,如果对测试驱动开发感兴趣推荐阅读【测试驱动开发的艺术】。
在实际工作中,应该只对一些复杂的功能进行单元测试,对于一些简单的功能(如数据访问层的CRUD)没有必要花费时间进行单元测试。
Spring对单元测试提供如下支持:
- Mock对象:Spring通过Mock对象来简化一些场景的单元测试:
JNDI测试支持:在org.springframework.mock.jndi包下通过了SimpleNamingContextBuilder来来创建JNDI上下文Mock对象,从而无需依赖特定Java EE容器即可完成JNDI测试。
web测试支持:在org.springframework.mock.web包中提供了一组Servlet API的Mock对象,从而可以无需Web容器即可测试web层的类。
- 工具类:通过通用的工具类来简化编写测试代码:
反射工具类:在org.springframework.test.util包下的ReflectionTestUtils能通过反射完成类的非public字段或setter方法的调用;
JDBC工具类:在org.springframework.test.util包下的SimpleJdbcTestUtils能读取一个sql脚本文件并执行来简化SQL的执行,还提供了如清空表、统计表中行数的简便方法来简化测试代码的编写。
接下来让我们学习一下开发过程中各层代码如何编写测试用例。
13.2.2 准备测试环境
1、Junit安装:将Junit 4包添加到“pointShop”项目中,具体方法请参照【2.2.3 Hello World】。
2、jMock安装:到jMock官网【http://www.jmock.org/】下载最新的jMock包,在本书中使用jMock2.5.1版本,将下载的“jmock-2.5.1-jars.zip ”包中的如下jar包拷贝到项目的lib目录下并添加到类路径:
objenesis-1.0.jarjmock-script-2.5.1.jarjmock-legacy-2.5.1.jarjmock-junit4-2.5.1.jarjmock-junit3-2.5.1.jarjmock-2.5.1.jarhamcrest-library-1.1.jarhamcrest-core-1.1.jarbsh-core-2.0b4.jar
注:cglib包无需添加到类路径,因为我们之前已经提供。
3、添加Spring测试支持包:将下载的spring-framework-3.0.5.RELEASE-with-docs.zip包中的如下jar包拷贝到项目的lib目录下并添加到类路径:
dist\org.springframework.test-3.0.5.RELEASE.jar
4、在“pointShop”项目下新建test文件夹并将其添加到【Java Build Path】中,该文件夹用于存放测试代码,从而分离测试代码和开发代码。
到此测试环境搭建完毕。
13.2.3 数据访问层
数据访问层单元测试,目的是测试该层定义的接口实现方法的行为是否正确,其实本质是测试是否正确与数据库交互,是否发送并执行了正确的SQL,SQL执行成功后是否正确的组装了业务逻辑层需要的数据。
数据访问层单元测试通过Mock对象与数据库交互的API来完成测试。
接下来让我们学习一下如何进行数据访问层单元测试:
1、在test文件夹下创建如下测试类:
package cn.javass.point.dao.hibernate; //省略import public class GoodsHibernateDaoUnitTest { //1、Mock对象上下文,用于创建Mock对象 private final Mockery context = new Mockery() {{ //1.1、表示可以支持Mock非接口类,默认只支持Mock接口 setImposteriser(ClassImposteriser.INSTANCE); }}; //2、Mock HibernateTemplate类 private final HibernateTemplate mockHibernateTemplate = context.mock(HibernateTemplate.class); private IGoodsDao goodsDao = null; @Before public void setUp() { //3、创建IGoodsDao实现 GoodsHibernateDao goodsDaoTemp = new GoodsHibernateDao(); //4、通过ReflectionTestUtils注入需要的非public字段数据 ReflectionTestUtils.setField(goodsDaoTemp, "entityClass", GoodsModel.class); //5、注入mockHibernateTemplate对象 goodsDaoTemp.setHibernateTemplate(mockHibernateTemplate); //6、赋值给我们要使用的接口 goodsDao = goodsDaoTemp; } }
- Mockery:jMock核心类,用于创建Mock对象的,通过其mock方法来创建相应接口或类的Mock对象。
- goodsDaoTemp:需要测试的IGoodsDao实现,通过ReflectionTestUtils注入需要的非public字段数据。
2、测试支持写完后,接下来测试一下IGoodsDao的get方法是否满足需求:
@Test public void testSave () { //7、创建需要的Model数据 final GoodsModel expected = new GoodsModel(); //8、定义预期行为,并在后边来验证预期行为是否正确 context.checking(new org.jmock.Expectations() { { //9、表示需要调用且只调用一次mockHibernateTemplate的get方法, //且get方法参数为(GoodsModel.class, 1),并将返回goods one(mockHibernateTemplate).get(GoodsModel.class, 1); will(returnValue(expected)); } }); //10、调用goodsDao的get方法,在内部实现中将委托给 //getHibernateTemplate().get(this.entityClass, id); //因此按照第8步定义的预期行为将返回goods GoodsModel actual = goodsDao.get(1); //11、来验证第8步定义的预期行为是否调用了 context.assertIsSatisfied(); //12、验证goodsDao.get(1)返回结果是否正确 Assert.assertEquals(goods, expected); }
- context.checking():该方法中用于定义预期行为,其中第9步定义了需要调用一次且只调用一次mockHibernateTemplate的get方法,且get方法参数为(GoodsModel.class, 1),并将返回goods对象。
- goodsDao.get(1):调用goodsDao的get方法,在内部实现中将委托给“getHibernateTemplate().get(this.entityClass, id)”。
- context.assertIsSatisfied():来验证前边定义的预期行为是否执行,且是否正确。
- Assert.assertEquals(expected, actual):用于验证“goodsDao.get(1) ”返回的结果是否是预期结果。
以上测试方法其实是没有必要的,对于非常简单的CRUD没有必要写单元测试,只有相当复杂的方法才有必要写单元测试。
这种通过Mock对象来测试数据访问层代码其实一点意义没有,因为这里没有与数据库交互,无法验证真实环境中与数据库交互是否正确,因此这里只是告诉你如何测试数据访问层代码,在实际工作中一般通过集成测试来完成数据访问层测试。
13.2.4 业务逻辑层
业务逻辑单元测试,目的是测试该层的业务逻辑是否正确并通过Mock 数据访问层对象来隔离与数据库交互,从而无需连接数据库即可测试业务逻辑是否正确。
接下来让我们学习一下如何进行业务逻辑层单元测试:
1、在test文件夹下创建如下测试类:
package cn.javass.point.service.impl; //省略import public class GoodsCodeServiceImplUnitTest { //1、Mock对象上下文,用于创建Mock对象 private final Mockery context = new Mockery() {{ //1.1、表示可以支持Mock非接口类,默认只支持Mock接口 setImposteriser(ClassImposteriser.INSTANCE); }}; //2、Mock IGoodsCodeDao接口 private IGoodsCodeDao goodsCodeDao = context.mock(IGoodsCodeDao.class);; private IGoodsCodeService goodsCodeService; @Before public void setUp() { GoodsCodeServiceImpl goodsCodeServiceTemp = new GoodsCodeServiceImpl(); //3、依赖注入 goodsCodeServiceTemp.setDao(goodsCodeDao); goodsCodeService = goodsCodeServiceTemp; } }
以上测试支持代码和数据访问层测试代码非常类似,在此不再阐述。
2、测试支持写完后,接下来测试一下购买商品Code码是否满足需求:
测试业务逻辑时需要分别测试多种场景,即如在某种场景下成功或失败等等,即测试应该全面,每个功能点都应该测试到。
2.1、测试购买失败的场景:
@Test(expected = NotCodeException.class) public void testBuyFail() { final int goodsId = 1; //4、定义预期行为,并在后边来验证预期行为是否正确 context.checking(new org.jmock.Expectations() { { //5、表示需要调用goodsCodeDao对象的getOneNotExchanged一次且仅以此 //且返回值为null one(goodsCodeDao).getOneNotExchanged(goodsId); will(returnValue(null)); } }); goodsCodeService.buy("test", goodsId); context.assertIsSatisfied(); }
- context.checking():该方法中用于定义预期行为,其中第5步定义了需要调用一次且只调用一次goodsCodeDao的getOneNotExchanged方法,且getOneNotExchanged方法参数为(goodsId),并将返回null。
- goodsCodeService.buy("test", goodsId):调用goodsCodeService的buy方法,由于调用goodsCodeDao的getOneNotExchanged方法将返回null,因此buy方法将抛出“NotCodeException”异常,从而表示没有Code码。
- context.assertIsSatisfied():来验证前边定义的预期行为是否执行,且是否正确。
- 由于我们在预期行为中调用getOneNotExchanged将返回null,因此测试将失败且抛出NotCodeException异常。
2.2、测试购买成功的场景:
@Test() public void testBuySuccess () { final int goodsId = 1; final GoodsCodeModel goodsCode = new GoodsCodeModel(); //6、定义预期行为,并在后边来验证预期行为是否正确 context.checking(new org.jmock.Expectations() { { //7、表示需要调用goodsCodeDao对象的getOneNotExchanged一次且仅以此 //且返回值为null one(goodsCodeDao).getOneNotExchanged(goodsId); will(returnValue(goodsCode)); //8、表示需要调用goodsCodeDao对象的save方法一次且仅一次 //且参数为goodsCode one(goodsCodeDao).save(goodsCode); } }); goodsCodeService.buy("test", goodsId); context.assertIsSatisfied(); Assert.assertEquals(goodsCode.isExchanged(), true); }
- context.checking():该方法中用于定义预期行为,其中第7步定义了需要调用一次且只调用一次goodsCodeDao的getOneNotExchanged方法,且getOneNotExchanged方法参数为(goodsId),并将返回goodsCode对象;第8步定义了需要调用goodsCodeDao对象的save一次且仅一次。
- goodsCodeService.buy("test", goodsId):调用goodsCodeService的buy方法,由于调用goodsCodeDao的getOneNotExchanged方法将返回goodsCode,因此buy方法将成功执行。
- context.assertIsSatisfied():来验证前边定义的预期行为是否执行,且是否正确。
- Assert.assertEquals(goodsCode.isExchanged(), true):表示goodsCode已经被购买过了。
- 由于我们在预期行为中调用getOneNotExchanged将返回一个goodsCode对象,因此测试将成功,如果失败说明业务逻辑写错了。
到此业务逻辑层测试完毕,在进行业务逻辑层测试时我们只关心业务逻辑是否正确,而不关心底层数据访问层如何实现,因此测试业务逻辑层时只需Mock 数据访问层对象,然后定义一些预期行为来满足业务逻辑测试需求即可。
13.2.5 表现层
表现层测试包括如Struts2的Action单元测试、拦截器单元测试、JSP单元测试等等,在此我们只学习Struts2的Action单元测试。
Struts2的Action测试相对业务逻辑层测试相对复杂一些,因为牵扯到使用如Servlet API、ActionContext等等,这里需要通过stub(桩)实现或mock对象来模拟如HttpServletRequest等对象。
一、首先学习一些最简单的Action测试:
1、在test文件夹下创建如下测试类:
package cn.javass.point.web.front; import cn.javass.point.service.IGoodsCodeService; import cn.javass.point.web.front.action.GoodsAction; //省略部分import public class GoodsActionUnitTest { //1、Mock对象上下文,用于创建Mock对象 private final Mockery context = new Mockery() {{ //1.1、表示可以支持Mock非接口类,默认只支持Mock接口 setImposteriser(ClassImposteriser.INSTANCE); }}; //2、Mock IGoodsCodeService接口 private IGoodsCodeService goodsCodeService = context.mock(IGoodsCodeService.class); private GoodsAction goodsAction; @Before public void setUp() { goodsAction = new GoodsAction(); //3、依赖注入 goodsAction.setGoodsCodeService(goodsCodeService); } }
以上测试支持代码和业务逻辑层测试代码非常类似,在此不再阐述。
2、测试支持写完后,接下来测试一下前台购买商品Code码是否满足需求:
类似于测试业务逻辑时需要分别测试多种场景,测试Action同样需要分别测试多种场景。
2.1、测试购买失败的场景:
@Test public void testBuyFail() { final int goodsId = 1; //4、定义预期行为,并在后边来验证预期行为是否正确 context.checking(new org.jmock.Expectations() { { //5、表示需要调用goodsCodeService对象的buy方法一次且仅一次 //且抛出NotCodeException异常 one(goodsCodeService).buy("test", goodsId); will(throwException(new NotCodeException())); } }); //6、模拟Struts注入请求参数 goodsAction.setGoodsId(goodsId); String actualResultCode = goodsAction.buy(); context.assertIsSatisfied(); Assert.assertEquals(ReflectionTestUtils.getField(goodsAction, "BUY_RESULT"), actualResultCode); Assert.assertTrue(goodsAction.getActionErrors().size() > 0); }
- context.checking():该方法中用于定义预期行为,其中第5步定义了需要调用goodsCodeService对象的buy方法一次且仅一次且将抛出NotCodeException异常。
- goodsAction.setGoodsId(goodsId):用于模拟Struts注入请求参数,即完成数据绑定。
- goodsAction.buy():调用goodsAction的buy方法,该方法将委托给IGoodsCodeService实现完成,返回值用于定位视图。
- context.assertIsSatisfied():来验证前边定义的预期行为是否执行,且是否正确。
- Assert.assertEquals(ReflectionTestUtils.getField(goodsAction, "BUY_RESULT"), actualResultCode):验证返回的Result是否是我们指定的。
- Assert.assertTrue(goodsAction.getActionErrors().size() > 0):表示执行Action时有错误,即Action动作错误。如果条件不成立,说明我们Action功能是错误的,需要修改。
2.2、测试购买成功的场景:
@Test public void testBuySuccess() { final int goodsId = 1; final GoodsCodeModel goodsCode = new GoodsCodeModel(); //7、定义预期行为,并在后边来验证预期行为是否正确 context.checking(new org.jmock.Expectations() { { //8、表示需要调用goodsCodeService对象的buy方法一次且仅一次 //且返回goodsCode对象 one(goodsCodeService).buy("test", goodsId); will(returnValue(goodsCode)); } }); //9、模拟Struts注入请求参数 goodsAction.setGoodsId(goodsId); String actualResultCode = goodsAction.buy(); context.assertIsSatisfied(); Assert.assertEquals(ReflectionTestUtils.getField(goodsAction, "BUY_RESULT"), actualResultCode); Assert.assertTrue(goodsAction.getActionErrors().size() == 0); }
- context.checking():该方法中用于定义预期行为,其中第5步定义了需要调用goodsCodeService对象的buy方法一次且仅一次且将返回goodsCode对象。
- goodsAction.setGoodsId(goodsId):用于模拟Struts注入请求参数,即完成数据绑定。
- goodsAction.buy():调用goodsAction的buy方法,该方法将委托给IGoodsCodeService实现完成,返回值用于定位视图。
- context.assertIsSatisfied():来验证前边定义的预期行为是否执行,且是否正确。
- Assert.assertEquals(ReflectionTestUtils.getField(goodsAction, "BUY_RESULT"), actualResultCode):验证返回的Result是否是我们指定的。
- Assert.assertTrue(goodsAction.getActionErrors().size() == 0):表示执行Action时没有错误,即Action动作正确。如果条件不成立,说明我们Action功能是错误的,需要修改。
通过模拟ActionContext对象内容从而可以非常容易的测试Action中各种与http请求相关情况,无需依赖web服务器即可完成测试。但对于如果我们使用htpp请求相关对象的该如何测试?如果我们需要使用ActionContext获取值栈数据应该怎么办?这就需要Struts提供的junit插件支持了。我们会在集成测试中介绍。
对于表现层其他功能的单元测试本书不再介绍,如JSP单元测试、拦截器单元测试等等。
13.3 集成测试
13.3.1 概述
集成测试是在单元测试之上,通常是将一个或多个已进行过单元测试的组件组合起来完成的,即集成测试中一般不会出现Mock对象,都是实实在在的真实实现。
对于单元测试,如前边在进行数据访问层单元测试时,通过Mock HibernateTemplate对象然后将其注入到相应的DAO实现,此时单元测试只测试某层的某个功能是否正确,对其他层如何提供服务采用Mock方式提供。
对于集成测试,如要进行数据访问层集成测试时,需要实实在在的HibernateTemplate对象然后将其注入到相应的DAO实现,此时集成测试将不仅测试该层功能是否正确,还将测试服务提供者提供的服务是否正确执行。
使用Spring的一个好处是能非常简单的进行集成测试,无需依赖web服务器或应用服务器即可完成测试。Spring通过提供一套TestContext框架来简化集成测试,使用TestContext测试框架能获得许多好处,如Spring IoC容器缓存、事务管理、依赖注入、Spring测试支持类等等。
13.3.2 Spring TestContext框架支持
Spring TestContext框架提供了一些通用的集成测试支持,主要提供如下支持:
一、上下文管理及缓存:
对于每一个测试用例(测试类)应该只有一个上下文,而不是每个测试方法都创建新的上下文,这样有助于减少启动容器的开销,提供测试效率。可通过如下方式指定要加载的上下文:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration( locations={"classpath:applicationContext-resources-test.xml", "classpath:cn/javass/point/dao/applicationContext-hibernate.xml"}) public class GoodsHibernateDaoIntegrationTest { }
- locations:指定Spring配置文件位置;
- inheritLocations:如果设置为false,将屏蔽掉父类中使用该注解指定的配置文件位置,默认为true表示继承父类中使用该注解指定的配置文件位置。
二、Test Fixture(测试固件)的依赖注入:
Test Fixture可以指运行测试时需要的任何东西,一般通过@Before定义的初始化Fixture方法准备这些资源,而通过@After定义的销毁Fixture方法销毁或还原这些资源。
Test Fixture的依赖注入就是使用Spring IoC容器的注入功能准备和销毁这些资源。可通过如下方式注入Test Fixture:
@Autowired private IGoodsDao goodsDao; @Autowired private ApplicationContext ctx;
即可以通过Spring提供的注解实现Bean的依赖注入来完成Test Fixture的依赖注入。
三、事务管理:
开启测试类的事务管理支持,即使用Spring 容器的事务管理功能,从而可以独立于应用服务器完成事务相关功能的测试。为了使测试中的事务管理起作用需要通过如下方式开启测试类事务的支持:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration( locations={"classpath:applicationContext-resources-test.xml", "classpath:cn/javass/point/dao/applicationContext-hibernate.xml"}) @TransactionConfiguration( transactionManager = "txManager", defaultRollback=true) public class GoodsHibernateDaoIntegrationTest { }
Spring提供如下事务相关注解来支持事务管理:
- @Transactional:使用@Transactional注解的类或方法将得到事务支持
- transactionManager:指定事务管理器;
- defaultRollback:是否回滚事务,默认为true表示回滚事务。
Spring还通过提供如下注解来简化事务测试:
- @Transactional:使用@Transactional注解的类或方法表示需要事务支持;
- @NotTransactional:只能注解方法,使用@NotTransactional注解的方法表示不需要事务支持,即不运行在事务中,Spring 3开始已不推荐使用;
- @BeforeTransaction和@AfterTransaction:使用这两个注解注解的方法定义了在一个事务性测试方法之前或之后执行的行为,且被注解的方法将运行在该事务性方法的事务之外。
- @Rollback(true):默认为true,用于替换@TransactionConfiguration中定义的defaultRollback指定的回滚行为。
四、常用注解支持:Spring框架提供如下注解来简化集成测试:
- @DirtiesContext:表示每个测试方法执行完毕需关闭当前上下文并重建一个全新的上下文,即不缓存上下文。可应用到类或方法级别,但在JUnit 3.8中只能应用到方法级别。
- @ExpectedException:表示被注解的方法预期将抛出一个异常,使用如@ExpectedException(NotCodeException.class)来指定异常,定义方式类似于Junit 4中的@Test(expected = NotCodeException.class),@ExpectedException注解和@Test(expected =……)应该两者选一。
- @Repeat:表示被注解的方法应被重复执行多少次,使用如@Repeat(2)方式指定。
- @Timed:表示被注解的方法必须在多长时间内运行完毕,超时将抛出异常,使用如@Timed(millis=10)方式指定,单位为毫秒。注意此处指定的时间是如下方法执行时间之和:测试方法执行时间(或者任何测试方法重复执行时间之和)、@Before和@After注解的测试方法之前和之后执行的方法执行时间。而Junit 4中的@Test(timeout=2)指定的超时时间只是测试方法执行时间,不包括任何重复等。
- 除了支持如上注解外,还支持【第十二章 零配置】中依赖注入等注解。
五、TestContext框架支持类:提供对测试框架的支持,如Junit、TestNG测试框架,用于集成Spring TestContext和测试框架来简化测试,TestContext框架提供如下支持类:
- JUnit 3.8支持类:提供对Spring TestContext框架与Junit3.8测试框架的集成:
AbstractJUnit38SpringContextTests:我们的测试类继承该类后将获取到Test Fixture的依赖注入好处。
AbstractTransactionalJUnit38SpringContextTests:我们的测试类继承该类后除了能得到Test Fixture的依赖注入好处,还额外获取到事务管理支持。
- JUnit 4.5+支持类:提供对Spring TestContext框架与Junit4.5+测试框架的集成:
AbstractJUnit4SpringContextTests:我们的测试类继承该类后将获取到Test Fixture的依赖注入好处。
AbstractTransactionalJUnit4SpringContextTests:我们的测试类继承该类后除了能得到Test Fixture的依赖注入好处,还额外获取到事务管理支持。
- 定制 Junit4.5+运行器:通过定制自己的Junit4.5+运行器从而无需继承JUnit 4.5+支持类即可完成需要的功能,如Test Fixture的依赖注入、事务管理支持,
@RunWith(SpringJUnit4ClassRunner.class):使用该注解注解到测试类上表示将集成Spring TestContext和Junit 4.5+测试框架。
@TestExecutionListeners:该注解用于指定TestContext框架的监听器用于与TestContext框架管理器发布的测试执行事件进行交互,TestContext框架提供如下三个默认的监听器:DependencyInjectionTestExecutionListener、DirtiesContextTestExecutionListener、TransactionalTestExecutionListener分别完成对Test Fixture的依赖注入、@DirtiesContext支持和事务管理支持,即在默认情况下将自动注册这三个监听器,另外还可以使用如下方式指定监听器:
@RunWith(SpringJUnit4ClassRunner.class) @TestExecutionListeners({}) public class GoodsHibernateDaoIntegrationTest { }
如上配置将通过定制的Junit4.5+运行器运行,但不会完成Test Fixture的依赖注入、事务管理等等,如果只需要Test Fixture的依赖注入,可以使用@TestExecutionListeners({DependencyInjectionTestExecutionListener.class})指定。
- TestNG支持类:提供对Spring TestContext框架与TestNG测试框架的集成:
AbstractTestNGSpringContextTests:我们的测试类继承该类后将获取到Test Fixture的依赖注入好处。
AbstractTransactionalTestNGSpringContextTests:我们的测试类继承该类后除了能得到Test Fixture的依赖注入好处,还额外获取到事务管理支持。
到此Spring TestContext测试框架减少完毕了,接下来让我们学习一下如何进行集成测试吧。
13.3.3 准备集成测试环境
对于集成测试环境各种配置应该和开发环境或实际生产环境配置相分离,即集成测试时应该使用单独搭建一套独立的测试环境,不应使用开发环境或实际生产环境的配置,从而保证测试环境、开发、生产环境相分离。
1、拷贝一份Spring资源配置文件applicationContext-resources.xml,并命名为applicationContext-resources-test.xml表示用于集成测试使用,并修改如下内容:
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <list> <value>classpath:resources-test.properties</value> </list> </property> </bean>
2、拷贝一份替换配置元数据的资源文件(resources/resources.properties),并命名为resources-test.properties表示用于集成测试使用,并修改为以下内容:
db.driver.class=org.hsqldb.jdbcDriver db.url=jdbc:hsqldb:mem:point_shop db.username=sa db.password= #Hibernate属性 hibernate.dialect=org.hibernate.dialect.HSQLDialect hibernate.hbm2ddl.auto=create-drop hibernate.show_sql=false hibernate.format_sql=true
- jdbc:hsqldb:mem:point_shop:我们在集成测试时将使用HSQLDB,并采用内存数据库模式运行;
- hibernate.hbm2ddl.auto=create-drop:表示在创建SessionFactory时根据Hibernate映射配置创建相应Model的表结构,并在SessionFactory关闭时删除这些表结构。
到此我们测试环境修改完毕,在进行集成测试时一定要保证测试环境、开发环境、实际生产环境相分离,即对于不同的环境使用不同的配置文件。
13.3.4 数据访问层
数据访问层集成测试,同单元测试一样目的不仅测试该层定义的接口实现方法的行为是否正确,而且还要测试是否正确与数据库交互,是否发送并执行了正确的SQL,SQL执行成功后是否正确的组装了业务逻辑层需要的数据。
数据访问层集成测试不再通过Mock对象与数据库交互的API来完成测试,而是使用实实在在存在的与数据库交互的对象来完成测试。
接下来让我们学习一下如何进行数据访问层集成测试:
1、在test文件夹下创建如下测试类:
package cn.javass.point.dao.hibernate; //省略import @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration( locations={"classpath:applicationContext-resources-test.xml", "classpath:cn/javass/point/dao/applicationContext-hibernate.xml"}) @TransactionConfiguration(transactionManager = "txManager", defaultRollback=false) public class GoodsHibernateDaoIntegrationTest { @Autowired private ApplicationContext ctx; @Autowired private IGoodsCodeDao goodsCodeDao; }
- @RunWith(SpringJUnit4ClassRunner.class):表示使用自己定制的Junit4.5+运行器来运行测试,即完成Spring TestContext框架与Junit集成;
- @ContextConfiguration:指定要加载的Spring配置文件,此处注意我们的Spring资源配置文件为“applicationContext-resources-test.xml”;
- @TransactionConfiguration:开启测试类的事务管理支持配置,并指定事务管理器和默认回滚行为;
- @Autowired:完成Test Fixture(测试固件)的依赖注入。
2、测试支持写完后,接下来测试一下分页查询所有已发布的商品是否满足需求:
@Transactional @Rollback @Test public void testListAllPublishedSuccess() { GoodsModel goods = new GoodsModel(); goods.setDeleted(false); goods.setDescription(""); goods.setName("测试商品"); goods.setPublished(true); goodsDao.save(goods); Assert.assertTrue(goodsDao.listAllPublished(1).size() == 1); Assert.assertTrue(goodsDao.listAllPublished(2).size() == 0); }
- @Transactional:表示测试方法将允许在事务环境;
- @Rollback:表示替换@ContextConfiguration指定的默认事务回滚行为,即将在测试方法执行完毕时回滚事务。
数据访问层的集成测试也是非常简单,与数据访问层的单元测试类似,也应该只对复杂的数据访问层代码进行测试。
13.3.5 业务逻辑层
业务逻辑层集成测试,目的同样是测试该层的业务逻辑是否正确,对于数据访问层实现通过Spring IoC容器完成装配,即使用真实的数据访问层实现来获取相应的底层数据。
接下来让我们学习一下如何进行业务逻辑层集成测试:
1、在test文件夹下创建如下测试类:
@ContextConfiguration( locations={"classpath:applicationContext-resources-test.xml", "classpath:cn/javass/point/dao/applicationContext-hibernate.xml", "classpath:cn/javass/point/service/applicationContext-service.xml"}) @TransactionConfiguration(transactionManager = "txManager", defaultRollback=false) public class GoodsCodeServiceImplIntegrationTest extends AbstractJUnit4SpringContextTests { @Autowired private IGoodsCodeService goodsCodeService; @Autowired private IGoodsService goodsService; }
- AbstractJUnit4SpringContextTests:表示将Spring TestContext框架与Junit4.5+测试框架集成;
- @ContextConfiguration:指定要加载的Spring配置文件,此处注意我们的Spring资源配置文件为“applicationContext-resources-test.xml”;
- @TransactionConfiguration:开启测试类的事务管理支持配置,并指定事务管理器和默认回滚行为;
- @Autowired:完成Test Fixture(测试固件)的依赖注入。
2、测试支持写完后,接下来测试一下购买商品Code码是否满足需求:
2.1、测试购买失败的场景:
- @Transactional
- @Rollback
- @ExpectedException(NotCodeException.class)
- @Test
- public void testBuyFail() {
- goodsCodeService.buy("test", 1);
- }
|
由于我们数据库中没有相应商品的Code码,因此将抛出NotCodeException异常。
2.2、测试购买成功的场景:
- @Transactional
- @Rollback
- @Test
- public void testBuySuccess() {
- //1.添加商品
- GoodsModel goods = new GoodsModel();
- goods.setDeleted(false);
- goods.setDescription("");
- goods.setName("测试商品");
- goods.setPublished(true);
- goodsService.save(goods);
- //2.添加商品Code码
- GoodsCodeModel goodsCode = new GoodsCodeModel();
- goodsCode.setGoods(goods);
- goodsCode.setCode("test");
- goodsCodeService.save(goodsCode);
- //3.测试购买商品Code码
- GoodsCodeModel resultGoodsCode = goodsCodeService.buy("test", 1);
- Assert.assertEquals(goodsCode.getId(), resultGoodsCode.getId());
- }
|
由于我们添加了指定商品的Code码因此购买将成功,如果失败说明业务写错了,应该重写。
业务逻辑层的集成测试也是非常简单,与业务逻辑层的单元测试类似,也应该只对复杂的业务逻辑层代码进行测试。
13.3.5 表现层
对于表现层集成测试,同样类似于单元测试,但对于业务逻辑层都将使用真实的实现,而不再是通过Mock对象来测试,这也是集成测试和单元测试的区别。
接下来让我们学习一下如何进行表现层Action集成测试:
1、准备Struts提供的junit插件, 到struts-2.2.1.1.zip中拷贝如下jar包到类路径:
|
2、测试支持类:Struts2提供StrutsSpringTestCase测试支持类,我们所有的Action测试类都需要继承该类;
3、准备Spring配置文件:由于我们的测试类继承StrutsSpringTestCase且将通过覆盖该类的getContextLocations方法来指定Spring配置文件,但由于getContextLocations方法只能返回一个配置文件,因此我们需要新建一个用于导入其他Spring配置文件的配置文件applicationContext-test.xml,具体内容如下:
- <import resource="classpath:applicationContext-resources-test.xml"/>
- <import resource="classpath:cn/javass/point/dao/applicationContext-hibernate.xml"/>
- <import resource="classpath:cn/javass/point/service/applicationContext-service.xml"/>
- <import resource="classpath:cn/javass/point/web/pointShop-admin-servlet.xml"/>
- <import resource="classpath:cn/javass/point/web/pointShop-front-servlet.xml"/>
3、在test文件夹下创建如下测试类:
- package cn.javass.point.web.front;
- //省略import
- @RunWith(SpringJUnit4ClassRunner.class)
- @TestExecutionListeners({})
- public class GoodsActionIntegrationTest extends StrutsSpringTestCase {
- @Override
- protected String getContextLocations() {
- return "classpath:applicationContext-test.xml";
- }
- @Before
- public void setUp() throws Exception {
- //1 指定Struts2配置文件
- //该方式等价于通过web.xml中的<init-param>方式指定参数
- Map<String, String> dispatcherInitParams = new HashMap<String, String>();
- ReflectionTestUtils.setField(this, "dispatcherInitParams", dispatcherInitParams);
- //1.1 指定Struts配置文件位置
- dispatcherInitParams.put("config", "struts-default.xml,struts-plugin.xml,struts.xml");
- super.setUp();
- }
- @After
- public void tearDown() throws Exception {
- super.tearDown();
- }
- }
- @RunWith(SpringJUnit4ClassRunner.class):表示使用自己定制的Junit4.5+运行器来运行测试,即完成Spring TestContext框架与Junit集成;
- @TestExecutionListeners({}):没有指定任何监听器,即不会自动完成对Test Fixture的依赖注入、@DirtiesContext支持和事务管理支持;
- StrutsSpringTestCase:集成测试Struts2+Spring时所有集成测试类必须继承该类;
- setUp方法:在每个测试方法之前都执行的初始化方法,其中dispatcherInitParams用于指定等价于在web.xml中的<init-param>方式指定的参数;必须调用super.setUp()用于初始化Struts2和Spring环境。
- tearDown():在每个测试方法之前都执行的销毁方法,必须调用super.tearDown()来销毁Spring容器等。
4、测试支持写完后,接下来测试一下前台购买商品Code码是否满足需求:
4.1、测试购买失败的场景:
- @Test
- public void testBuyFail() throws UnsupportedEncodingException, ServletException {
- //2 前台购买商品失败
- //2.1 首先重置hhtp相关对象,并准备准备请求参数
- initServletMockObjects();
- request.setParameter("goodsId", String.valueOf(Integer.MIN_VALUE));
- //2.2 调用前台GoodsAction的buy方法完成购买相应商品的Code码
- executeAction("/goods/buy.action");
- GoodsAction frontGoodsAction = (GoodsAction) ActionContext.getContext().getActionInvocation().getAction();
- //2.3 验证前台GoodsAction的buy方法有错误
- Assert.assertTrue(frontGoodsAction.getActionErrors().size() > 0);
- }
- initServletMockObjects():用于重置所有http相关对象,如request等;
- request.setParameter("goodsId", String.valueOf(Integer.MIN_VALUE)):用于准备请求参数;
- executeAction("/goods/buy.action"):通过模拟http请求来调用前台GoodsAction的buy方法完成商品购买
- Assert.assertTrue(frontGoodsAction.getActionErrors().size() > 0):表示执行Action时有错误,即Action动作错误。如果条件不成立,说明我们Action功能是错误的,需要修改。
4.2、测试购买成功的场景:
- @Test
- public void testBuySuccess() throws UnsupportedEncodingException, ServletException {
- //3 后台新增商品
- //3.1 准备请求参数
- request.setParameter("goods.name", "测试商品");
- request.setParameter("goods.description", "测试商品描述");
- request.setParameter("goods.originalPoint", "1");
- request.setParameter("goods.nowPoint", "2");
- request.setParameter("goods.published", "true");
- //3.2 调用后台GoodsAction的add方法完成新增
- executeAction("/admin/goods/add.action");
- //2.3 获取GoodsAction的goods属性
- GoodsModel goods = (GoodsModel) findValueAfterExecute("goods");
- //4 后台新增商品Code码
- //4.1 首先重置hhtp相关对象,并准备准备请求参数
- initServletMockObjects();
- request.setParameter("goodsId", String.valueOf(goods.getId()));
- request.setParameter("codes", "a\rb");
- //4.2 调用后台GoodsCodeAction的add方法完成新增商品Code码
- executeAction("/admin/goodsCode/add.action");
- //5 前台购买商品成功
- //5.1 首先重置hhtp相关对象,并准备准备请求参数
- initServletMockObjects();
- request.setParameter("goodsId", String.valueOf(goods.getId()));
- //5.2 调用前台GoodsAction的buy方法完成购买相应商品的Code码
- executeAction("/goods/buy.action");
- GoodsAction frontGoodsAction = (GoodsAction) ActionContext.getContext().getActionInvocation().getAction();
- //5.3 验证前台GoodsAction的buy方法没有错误
- Assert.assertTrue(frontGoodsAction.getActionErrors().size() == 0);
- }
- executeAction("/admin/goods/add.action"):调用后台GoodsAction的add方法,用于新增商品;
- executeAction("/admin/goodsCode/add.action"):调用后台GoodCodeAction的add方法用于新增商品Code码;
- executeAction("/goods/buy.action"):调用前台GoodsAction的buy方法,用于购买相应商品,其中Assert.assertTrue(frontGoodsAction.getActionErrors().size() == 0)表示购买成功,即Action动作正确。
表现层Action集成测试介绍就到此为止,如何深入StrutsSpringTestCase来完成集成测试已超出本书范围,如果读者对这部分感兴趣可以到Struts2官网学习最新的测试技巧。