使用Spring的测试机制进行集成测试
2010-03-01 11:11
477 查看
Spring3.0已经在2009年12月中旬正式发布,但是目前的各种应用系统仍然基于2.5甚至更早的版本构建而来,并且从Sprin3.0的新特性(核心API迁移至java1.5及使用范型、Spring Expression Language、IoC(现可以使用java配置替换xml)、类型转换及格式化显示、声明式的Model验证(JSR303,Hibernate Validator)、完整的springmvc rest支持、支持嵌入式数据库(方便测试),看来确实是吸收了ruby之类的动态语言的优点及理念)来看,并没有对测试机制做太大改动。对于2.5全新的基于注解(标记)的测试机制,之前转载了IBM DeveloperWorks的一篇相关文章
,本文不再赘述。考虑很多公司仍然在Spring2.0的环境下开发,本文介绍2.0以上版本通用的测试机制(如果还在使用2.0以前的版本,相信你的系统基本上不需要太多的集成测试了)。
在接触Spring的测试机制之前,一直直接使用JUnit进行集成测试,在获取需要测试的Bean时,都需要直接使用ApplicationContext的getBean()方法从Spirng容器中获取需要测试的目标Bean;甚至在获取Spring的配置文件时,不得不通过配置文件的绝对路径来寻找applicationContext.xml文件,这在团队开发中简直就是噩梦!另外,直接使用JUnit进行测试容易破坏数据库的现有结构,例如主键ID自增长的情况以及由于主键重复造成的同样的数据插入第二次时的异常等。Spring提供了一套基于JUnit扩展的测试机制,可以方便地实现Bean的获取、配置文件的获取、保护数据库现场、同一事务下访问数据库以检验业务操作的正确性等特性。
对IDevCompanyService的实现如下所示:
说明:系统持久层使用JPA(OpenJPA1.2实现),使用Spring的声明式事务。
Service中操作的开发公司实体Bean(DevCompany),内容如下:
集成测试往往针对某一个Service中的多个方法同时测试,为防止Spring上下文多次创建,同时针对集成测试的串行特点,简单使用了一个不是很正规的单例模式,貌似可以达到效果。结合JUnit的生命周期特点(关于JUnit的生命周期请参考了解JUnit核心类、接口及生命周期
中的详细说明),覆盖setUp方法,保证正常获取Spring上下文。
使用JUnit测试的开发公司Service测试类TestDevCompanyServiceJUnit,其代码如下所示:
上述代码,在第一次测试时,没有任何问题,但是由于数据库现场已经被破坏了,如果两次都执行添加的测试操作(或者测试方法中没有删除此条记录的操作)时,会抛出主键重复的异常;而且每个测试方法中都需要手工根据Service名获取Service对象,即使将Service对象设为全局变量也需要手工硬编码的方式获取;另外,上述测试只能保证被测试的方法正确执行了,对于执行后的结果是否真的正确,只能通过手工查看数据库来实现,对于简单的数据似乎没有问题,但是如果涉及到的数据量足够大,手工查看数据库的方式几乎不可能完成,当然可以使用assert*系列方法,但是assert方法也只是判断获取的对象的值是否正常,对于数据库中实际的值,不敢保证是否完全一致。
使用Spring提供的测试机制,便可以很好的解决上述问题,即:
Ø 保护数据库现场
Ø 通过IoC将Service注入来获取其对象,而不是硬编码
Ø 查看数据库中的实际值
Spring的测试机制是基于JUnit的扩展,在org.springframework.test包下,可以看到6个从TestCase基础上扩展出来的抽象类,分别是:ConditionalTestCase(可以有选择地关闭掉一些测试方法,不让他们在测试用例中执行,而无需将这些方法注释掉)、AbstractSpringContextTests(运行多个测试用例和测试方法时,Spring上下文只需创建一次)、AbstractSingleSpringContextTests(方便手工指定Spring配置文件、手工设定Spring容器是否需要重新加载)、AbstractDependencyInjectionSpringContextTests(自动装配、依赖检查、自动注入)、AbstractTransactionalSpringContextTests(自动恢复数据库现场即自动回滚)、AbstractTransactionalDataSourceSpringContextTests(通过JDBC访问数据库,检测数据操作正确性),上述抽象类按照先后顺序逐步加强了每个抽象类的功能,并且按照逐步继承的关系,使得子抽象类具有父抽象类的所有特性,因此最终的AbstractTransactionalDataSourceSpringContextTests抽象类具有其所有祖先抽象类的特性以及其自身的特性,实际应用中可以根据需要选择需要使用的抽象基类进行扩展。
基于AbstractDependencyInjectionSpringContextTests的扩展
多数系统在进行集成测试的前期,需要大量的测试数据,此时,并不需要自动回滚数据,因此可以直接使用抽象基类AbstractDependencyInjectionSpringContextTests的子类进行测试。此时的抽象基类已经具有Spring容器只创建一次、手工指定配置文件、自动装配、依赖检查、自动注入的功能,可以满足此时的测试需求。因此,简单的增删改查的操作,比较适合使用AbstractDependencyInjectionSpringContextTests进行集成测试。为更加方便测试,对AbstractDependencyInjectionSpringContextTests进行再次包装,创建一个测试用的抽象类,从而方便以后的实际测试。考虑到实际测试时需要加载不同的配置文件,此抽象类中需要一个用于子类补充的配置文件列表,并可以根据此列表,自动装载所有指定的配置文件;一个忽略测试的方法名列表。此抽象类AbstractServiceInjectionTest的代码如下所示:
具体测试时,继承此抽象类,完成对Service的测试。此时可以不用关注事务相关的问题。不用关注事务,使得AbstractDependencyInjectionSpringContextTests在某些情况下要比AbstractTransactionalSpringContextTests相对好用。
注意:
在实际测试时,发现如果在Spring配置文件中指定了如下所示的Spring使用的资源文件列表
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
file:
WebContent/WEB-INF/db.properties
则上文中的红色部分必须加上,否则系统会抛一个FileNotFoundException,而实际运行时,则必须将此处去掉。
基于AbstractTransactionalSpringContextTests的扩展
在系统开发的中后期,需要进行的测试,以各种负责的业务操作为主了,此时可能需要一些可以保护数据库现场的测试功能,而AbstractTransactionalSpringContextTests提供了一套数据库自动回滚的功能,方便恢复数据库现场,从而不必每次测试完成后都重新恢复数据库的操作。基于AbstractTransactionalSpringContextTests扩展的抽象类,同样需要提供一些较为通用的功能以方便使用,下面是扩展的抽象类的实现代码:
如果在测试时,需要将测试结果持久化,可以直接在测试的方法末尾加上“setComplete()”方法。由此,可以看出基于AbstractTransactionalSpringContextTests抽象基类的扩展,同样可以将数据持久化到数据库中,因此,实际测试中,可以使用此方式实现不启用自动事务回滚功能。针对需要测试的Service,编写的测试用例如下所示:
说明:
如果使用Spring的事务管理功能,此处在事务处理上可能会有一些冲突,例如将以“query”开头的方法的propagation设置为“NEVER”,测试时可能会造成事务自动回滚或者不回滚的异常,此时,需要做的操作是将以“query”开头的方法的propagation设置为默认的“REQUIRED”,同时加上read-only="true",可以解决此问题。
另外,AbstractTransactionalSpringContextTests提供了一些可以在事务处理过程中进行操作的各种入口。
onSetUpBeforeTransaction()与onTearDownAfterTransaction():子类可以覆盖这两个方法,可以在事务测试方法运行的前后执行一些数据库初始化的操作并在事务完成后将其清除;
onSetUpInTransaction()与onTearDownInTransaction():这对方法和上面介绍的方法完成的功能相同,只不过它们在测试方法的相同事务中执行的。
AbstractTransactionalSpringContextTests还提供了一组用于测试延迟数据加载的方法:endTransaction()与startNewTransaction()。当用户在测试Hibernate、JPA等允许延迟数据加载的应用时,模拟数据在Service层事务中被部分加载,当传递到Web层时重新打开事务完成延迟部分数据加载的测试场景。可以在测试方法中显式调用endTransaction()方法以模拟从Service层中获取部分数据后返回,然后,通过startNewTransaction()开启一个和原事务无关新事务——模拟在Web层中重新打开事务,接下来就可以访问延迟加载的数据,进行测试了。
基于AbstractTransactionalDataSourceSpringContextTests的扩展
AbstractTransactionalDataSourceSpringContextTests是目前Spring提供的测试机制中的终极方案,用户可以在此抽象类的子类中直接调用JdbcTemplate,自动使用Spring容器中的数据源(DataSource)创建好一个JdbcTemplate实例并开放给子类使用,方便用户进行JDBC相关操作,直接查看数据库中的数据是否正确。
对AbstractTransactionalDataSourceSpringContextTests做简单封装后的抽象类代码如下所示:
此时在集成测试时,可以直接使用JdbcTemplate获取数据库中的值,从而验证方法是否正确执行,同时此时仍然可以保证数据库现场不被破坏,需要测试的Service其测试用例如下所示:
使用JdbcTemplate直接获取数据库数据的方式,多用于更新以及复杂的业务逻辑中的数据验证。有了Spring的测试机制,对于使用Spring的应用,可以方便的进行集成测试,从而简化开发的工作量,减少繁琐的重复工作,提供工作效率。
注意:
如果采用byName的自动装配机制,数据源Bean的名称必须取名为“dataSource”。
另外,需要说明的是,Spring提供了另外一套在此基础上的基于注解的测试体系,用户在测试时直接使用注解可以更方便,而且使用注解还可以绕开由于事务的操作造成的某些问题(比如,本文提到的关于事务传播类型为“NEVER”的情况)。对于此,请参考另外一篇相关文章
。
,本文不再赘述。考虑很多公司仍然在Spring2.0的环境下开发,本文介绍2.0以上版本通用的测试机制(如果还在使用2.0以前的版本,相信你的系统基本上不需要太多的集成测试了)。
在接触Spring的测试机制之前,一直直接使用JUnit进行集成测试,在获取需要测试的Bean时,都需要直接使用ApplicationContext的getBean()方法从Spirng容器中获取需要测试的目标Bean;甚至在获取Spring的配置文件时,不得不通过配置文件的绝对路径来寻找applicationContext.xml文件,这在团队开发中简直就是噩梦!另外,直接使用JUnit进行测试容易破坏数据库的现有结构,例如主键ID自增长的情况以及由于主键重复造成的同样的数据插入第二次时的异常等。Spring提供了一套基于JUnit扩展的测试机制,可以方便地实现Bean的获取、配置文件的获取、保护数据库现场、同一事务下访问数据库以检验业务操作的正确性等特性。
需要测试的Service
下面以IDevCompanyService接口为例,介绍Spring的测试机制。IDevCompanyService是一个典型的业务层接口,含有6个方法,分别执行新增、修改、删除、查询等操作。IDevCompanyService代码如下所示:/** * IDevCompanyService.class */ package test.serviceTest.spring; import java.util.List; import test.serviceTest.spring.DevCompany; /** * 开发公司Service接口<br/> * 开发公司相关业务层方法,在此定义 * * @author cyq * */ public interface IDevCompanyService { /** * 新增开发企业信息 * * @param dev * @return * @throws Exception */ DevCompany addDevCompany(DevCompany dev) throws Exception; /** * 更新开发企业信息 * * @param dev * @return * @throws Exception */ DevCompany updateDevCompany(DevCompany dev) throws Exception; /** * 删除开发企业信息 * * @param id * @throws Exception */ void deleteDevCompanyByID(Long id) throws Exception; /** * 根据主键查看开发企业详细信息 * * @param id * @return * @throws Exception */ DevCompany queryDevCompanyByID(Long id) throws Exception; /** * 支持JPQL语句的分页查询 * * @param condition * 查询条件 * @param first * 起始索引 * @param size * 获取数量 * @param params * 条件值 * @return * @throws Exception */ List<DevCompany> queryDevCompanyForList(String condition, int starte, int size, Object... params) throws Exception; /** * 根据条件,统计结果的行数 * * @return * @throws Exception */ Long queryDevCompanyCount(String condition) throws Exception; }
对IDevCompanyService的实现如下所示:
/** * DevCompanyService.class */ package test.serviceTest.spring; import java.util.List; import org.apache.commons.lang.StringUtils; /** * 开发公司Service实现类 * * @author cyq * */ public class DevCompanyService implements IDevCompanyService { private IDevCompanyDAO devCompanyDao; /** * */ public DevCompanyService() { super(); } /* * (non-Javadoc) * * @see * test.serviceTest.spring.IDevCompanyService#addDevCompany(test.serviceTest * .spring.DevCompany) */ public DevCompany addDevCompany(DevCompany dev) throws Exception { return getDevCompanyDao().mergeEntity(DevCompany.class, dev); } /* * (non-Javadoc) * * @see * test.serviceTest.spring.IDevCompanyService#deleteDevCompanyByID(java. * lang.Long) */ public void deleteDevCompanyByID(Long id) throws Exception { getDevCompanyDao().removeEntity(DevCompany.class, id); } /* * (non-Javadoc) * * @see * test.serviceTest.spring.IDevCompanyService#queryDevCompanyByID(java.lang * .Long) */ public DevCompany queryDevCompanyByID(Long id) throws Exception { return getDevCompanyDao().findEntityByPk(DevCompany.class, id); } /* * (non-Javadoc) * * @see * test.serviceTest.spring.IDevCompanyService#queryDevCompanyCount(java. * lang.String) */ public Long queryDevCompanyCount(String condition) throws Exception { String jpql = "SELECT COUNT(d.id) FROM DevCompany d"; if (StringUtils.isNotBlank(condition)) jpql += " WHERE " + condition; return getDevCompanyDao().executeSelectStat(jpql); } /* * (non-Javadoc) * * @see * test.serviceTest.spring.IDevCompanyService#queryDevCompanyForList(java * .lang.String, int, int, java.lang.Object[]) */ @SuppressWarnings("unchecked") public List<DevCompany> queryDevCompanyForList(String condition, int starte, int size, Object... params) throws Exception { String jpql = "SELECT d FROM DevCompany d "; if (StringUtils.isNotBlank(condition)) jpql += " WHERE " + condition; return getDevCompanyDao().executeSelect(jpql, params); } /* * (non-Javadoc) * * @see * test.serviceTest.spring.IDevCompanyService#updateDevCompany(test.serviceTest * .spring.DevCompany) */ public DevCompany updateDevCompany(DevCompany dev) throws Exception { return getDevCompanyDao().updateEntity(DevCompany.class, dev); } public IDevCompanyDAO getDevCompanyDao() { return devCompanyDao; } public void setDevCompanyDao(IDevCompanyDAO devCompanyDao) { this.devCompanyDao = devCompanyDao; } }
说明:系统持久层使用JPA(OpenJPA1.2实现),使用Spring的声明式事务。
Service中操作的开发公司实体Bean(DevCompany),内容如下:
/** * DevCompany.class */ package test.serviceTest.spring; import java.io.Serializable; import java.util.Date; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.TemporalType; /** * 开发公司实体Bean * * @author cyq * */ @Entity @Table(name = "test_estate_c_devCompany") public class DevCompany implements Serializable { /** * */ private static final long serialVersionUID = 8365755236355333760L; /** * ID主键 */ private Long id; /** * 编号(唯一) */ private String code; /** * 公司名称 */ private String devName; /** * 成立日期 */ private Date setupDate; /** * 单位地址 */ private String address; /** * 注册操作员ID */ private Long operId; /** * 注册时间 */ private Date operDate; public DevCompany() { } public DevCompany(Long id, String code, String devName, Date setupDate, String address, Long operId, Date operDate) { this.id = id; this.code = code; this.devName = devName; this.setupDate = setupDate; this.address = address; this.operId = operId; this.operDate = operDate; } /** * ID主键 */ @Id @Column(name = "id") public Long getId() { return id; } public void setId(Long id) { this.id = id; } /** * 公司名称 */ @Column(name = "devName", length = 128, nullable = false) public String getDevName() { return devName; } public void setDevName(String devName) { this.devName = devName; } /** * 成立日期 */ @Temporal(TemporalType.DATE) @Column(name = "setupDate") public Date getSetupDate() { return setupDate; } public void setSetupDate(Date setupDate) { this.setupDate = setupDate; } /** * 单位地址 */ @Column(name = "address", length = 256) public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } /** * 注册操作员ID */ @Column(name = "operId") public Long getOperId() { return operId; } public void setOperId(Long operId) { this.operId = operId; } /** * 注册时间 */ @Temporal(TemporalType.DATE) @Column(name = "operDate") public Date getOperDate() { return operDate; } public void setOperDate(Date operDate) { this.operDate = operDate; } /** * 公司编号 * * @return */ @Column(name = "code", nullable = false, unique = true, length = 18) public String getCode() { return code; } public void setCode(String code) { this.code = code; } }
直接使用JUnit进行测试
直接使用JUnit进行测试时,测试类需要继承TestCase抽象类,从而可以被JUnit容器执行。为方便测试,封装了一个用于JUnit测试的抽象类AbstractServiceTestJUnit,以简化操作,其代码如下所示:/** * AbstractServiceTestJUnit.class */ package test.serviceTest.spring; import org.springframework.context.ApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; import junit.framework.TestCase; /** * 直接使用JUnit进行测试的抽象基类 * * @author cyq * */ public abstract class AbstractServiceTestJUnit extends TestCase { /** * applicationContext.xml文件的路径,<br/> * 如果配置文件没有放在classpath下,必须使用此写法;<br/> * 如果配置文件放在classpath下,则可以直接文件名 */ String applicationContextFile = "WebContent//WEB-INF//springConfig//applicationContext.xml"; /** * Spring上下文 */ static ApplicationContext ctx; /** * */ public AbstractServiceTestJUnit() { super(); } /** * @param name */ public AbstractServiceTestJUnit(String name) { super(name); } /** * applicationContext.xml文件以外的配置文件的路径列表 * * @return */ abstract String[] getOtherConfigFiles(); /** * 获取Spring上下文实例<br/> * 上下文实例简单使用Singleton,保证上下文只创建一次 * * @return */ ApplicationContext getApplicationContext() { if (ctx == null) { String[] otherConfigs = getOtherConfigFiles(); String[] configFiles = new String[otherConfigs.length + 1]; // 所有配置文件列表 configFiles[0] = applicationContextFile; System.arraycopy(otherConfigs, 0, configFiles, 1, otherConfigs.length); // 将所有的配置文件列表放入目标配置文件列表 ctx = new FileSystemXmlApplicationContext(configFiles); // 获取Spring上下文 } return ctx; } /** * 覆盖setUp方法,获取Spring上下文实例 */ protected void setUp() throws Exception { getApplicationContext(); } }
集成测试往往针对某一个Service中的多个方法同时测试,为防止Spring上下文多次创建,同时针对集成测试的串行特点,简单使用了一个不是很正规的单例模式,貌似可以达到效果。结合JUnit的生命周期特点(关于JUnit的生命周期请参考了解JUnit核心类、接口及生命周期
中的详细说明),覆盖setUp方法,保证正常获取Spring上下文。
使用JUnit测试的开发公司Service测试类TestDevCompanyServiceJUnit,其代码如下所示:
package test.serviceTest.spring; import java.text.SimpleDateFormat; import java.util.Date; /** * 使用JUnit测试开发公司Service * * @author cyq * */ public class TestDevCompanyServiceJUnit extends AbstractServiceTestJUnit { /** * */ public TestDevCompanyServiceJUnit() { super(); } /** * @param name */ public TestDevCompanyServiceJUnit(String name) { super(name); } /** * 配置文件列表 */ String[] getOtherConfigFiles() { return new String[] { "WebContent//WEB-INF//springConfig//corp//devCompany-context.xml" }; } /** * 测试添加开发公司的方法 */ public void testAddDevCompany() { System.out.println("test add method..."); IDevCompanyService devCompanyService = (IDevCompanyService) ctx .getBean("devCompanyServiceTest"); try { DevCompany entity = devCompanyService.addDevCompany(new DevCompany( new Long(1), "370101123456789098", "中国房地产开发总公司", new SimpleDateFormat("yyyy-MM-dd").parse("1990-01-02"), "山东省济南市", new Long(10), new Date())); assertNotNull(entity); assertTrue(entity.getId() > 0); } catch (Exception e) { e.printStackTrace(); } } /** * 测试查询方法 */ public void testQueryDevCompany() { System.out.println("test query method..."); IDevCompanyService devCompanyService = (IDevCompanyService) ctx .getBean("devCompanyServiceTest"); try { devCompanyService.queryDevCompanyByID(new Long(1)); Long counts = devCompanyService.queryDevCompanyCount(""); System.out.println("records size is:" + counts); assertTrue(counts > 0); devCompanyService.queryDevCompanyForList(null, 0, -1); } catch (Exception e) { e.printStackTrace(); } } /** * 测试更新方法 */ public void testUpdateDevCompany() { System.out.println("test update method..."); IDevCompanyService devCompanyService = (IDevCompanyService) ctx .getBean("devCompanyServiceTest"); try { DevCompany dc = devCompanyService.queryDevCompanyByID(new Long(1)); dc.setAddress(""); DevCompany entity = devCompanyService.updateDevCompany(dc); assertNotNull(entity); assertEquals("", entity.getAddress()); assertTrue("".equals(entity.getAddress())); } catch (Exception e) { e.printStackTrace(); } } /** * 测试删除方法 */ public void testDeleteDevCompany() { System.out.println("test delete method..."); IDevCompanyService devCompanyService = (IDevCompanyService) ctx .getBean("devCompanyServiceTest"); try { DevCompany dc = devCompanyService.queryDevCompanyByID(new Long(1)); devCompanyService.deleteDevCompanyByID(dc.getId()); assertNull(devCompanyService.queryDevCompanyByID(new Long(1))); } catch (Exception e) { e.printStackTrace(); } } }
上述代码,在第一次测试时,没有任何问题,但是由于数据库现场已经被破坏了,如果两次都执行添加的测试操作(或者测试方法中没有删除此条记录的操作)时,会抛出主键重复的异常;而且每个测试方法中都需要手工根据Service名获取Service对象,即使将Service对象设为全局变量也需要手工硬编码的方式获取;另外,上述测试只能保证被测试的方法正确执行了,对于执行后的结果是否真的正确,只能通过手工查看数据库来实现,对于简单的数据似乎没有问题,但是如果涉及到的数据量足够大,手工查看数据库的方式几乎不可能完成,当然可以使用assert*系列方法,但是assert方法也只是判断获取的对象的值是否正常,对于数据库中实际的值,不敢保证是否完全一致。
使用Spring提供的测试机制,便可以很好的解决上述问题,即:
Ø 保护数据库现场
Ø 通过IoC将Service注入来获取其对象,而不是硬编码
Ø 查看数据库中的实际值
使用Spring的测试机制
Spring的测试包为spring-test.jar文件(某些版本的测试包可能在spring-mock.jar下,具体应用时请检查对应的class文件是否存在),将此文件放入工程的lib下,既可以使用Spring的测试机制进行测试了。Spring的测试机制是基于JUnit的扩展,在org.springframework.test包下,可以看到6个从TestCase基础上扩展出来的抽象类,分别是:ConditionalTestCase(可以有选择地关闭掉一些测试方法,不让他们在测试用例中执行,而无需将这些方法注释掉)、AbstractSpringContextTests(运行多个测试用例和测试方法时,Spring上下文只需创建一次)、AbstractSingleSpringContextTests(方便手工指定Spring配置文件、手工设定Spring容器是否需要重新加载)、AbstractDependencyInjectionSpringContextTests(自动装配、依赖检查、自动注入)、AbstractTransactionalSpringContextTests(自动恢复数据库现场即自动回滚)、AbstractTransactionalDataSourceSpringContextTests(通过JDBC访问数据库,检测数据操作正确性),上述抽象类按照先后顺序逐步加强了每个抽象类的功能,并且按照逐步继承的关系,使得子抽象类具有父抽象类的所有特性,因此最终的AbstractTransactionalDataSourceSpringContextTests抽象类具有其所有祖先抽象类的特性以及其自身的特性,实际应用中可以根据需要选择需要使用的抽象基类进行扩展。
基于AbstractDependencyInjectionSpringContextTests的扩展
多数系统在进行集成测试的前期,需要大量的测试数据,此时,并不需要自动回滚数据,因此可以直接使用抽象基类AbstractDependencyInjectionSpringContextTests的子类进行测试。此时的抽象基类已经具有Spring容器只创建一次、手工指定配置文件、自动装配、依赖检查、自动注入的功能,可以满足此时的测试需求。因此,简单的增删改查的操作,比较适合使用AbstractDependencyInjectionSpringContextTests进行集成测试。为更加方便测试,对AbstractDependencyInjectionSpringContextTests进行再次包装,创建一个测试用的抽象类,从而方便以后的实际测试。考虑到实际测试时需要加载不同的配置文件,此抽象类中需要一个用于子类补充的配置文件列表,并可以根据此列表,自动装载所有指定的配置文件;一个忽略测试的方法名列表。此抽象类AbstractServiceInjectionTest的代码如下所示:
/** * AbstractServiceInjectionTest.class */ package test.serviceTest.spring; import org.springframework.test.AbstractDependencyInjectionSpringContextTests; /** * 基于AbstractDependencyInjectionSpringContextTests的抽象测试类 * * @author cyq * */ public abstract class AbstractServiceInjectionTest extends AbstractDependencyInjectionSpringContextTests { String filePathSufix = "file:WebContent/WEB-INF/springConfig/"; // 配置文件地址的前缀 String appContextFile = "file:WebContent/WEB-INF/springConfig/applicationContext.xml";// applicationContext.xml文件地址 /** * */ public AbstractServiceInjectionTest() { super(); } /** * @param name */ public AbstractServiceInjectionTest(String name) { super(name); } /** * 需要加载的配置文件地址列表 * * @return */ abstract String[] getOtherConfigs(); /** * 覆盖的获取配置文件地址的方法 */ protected String[] getConfigLocations() { String[] otherConfigs = getOtherConfigs(); String[] configFiles = new String[otherConfigs.length + 1]; // 所有配置文件列表 configFiles[0] = appContextFile; System.arraycopy(otherConfigs, 0, configFiles, 1, otherConfigs.length); return configFiles; } /** * 忽略的方法列表 * * @return */ abstract String[] getIgnoredMethods(); /** * 所有忽略方法列表中的方法都在测试时不执行 */ protected boolean isDisabledInThisEnvironment(String testMethodName) { for (String methodName : getIgnoredMethods()) { if (methodName.equals(testMethodName)) return true; } return false; } }
具体测试时,继承此抽象类,完成对Service的测试。此时可以不用关注事务相关的问题。不用关注事务,使得AbstractDependencyInjectionSpringContextTests在某些情况下要比AbstractTransactionalSpringContextTests相对好用。
注意:
在实际测试时,发现如果在Spring配置文件中指定了如下所示的Spring使用的资源文件列表
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
file:
WebContent/WEB-INF/db.properties
则上文中的红色部分必须加上,否则系统会抛一个FileNotFoundException,而实际运行时,则必须将此处去掉。
基于AbstractTransactionalSpringContextTests的扩展
在系统开发的中后期,需要进行的测试,以各种负责的业务操作为主了,此时可能需要一些可以保护数据库现场的测试功能,而AbstractTransactionalSpringContextTests提供了一套数据库自动回滚的功能,方便恢复数据库现场,从而不必每次测试完成后都重新恢复数据库的操作。基于AbstractTransactionalSpringContextTests扩展的抽象类,同样需要提供一些较为通用的功能以方便使用,下面是扩展的抽象类的实现代码:
/** * AbstractServiceTransactionalTest.class */ package test.serviceTest.spring; import org.springframework.test.AbstractTransactionalSpringContextTests; /** * 可以自动回滚事务的测试抽象基类 * * @author cyq * */ public abstract class AbstractServiceTransactionalTest extends AbstractTransactionalSpringContextTests { String filePathSufix = "file:WebContent/WEB-INF/springConfig/"; // 配置文件地址的前缀 String appContextFile = filePathSufix + "applicationContext.xml";// applicationContext.xml文件地址 /** * */ public AbstractServiceTransactionalTest() { super(); } /** * @param name */ public AbstractServiceTransactionalTest(String name) { super(name); } /** * 需要加载的配置文件地址列表 * * @return */ abstract String[] getOtherConfigs(); /** * 覆盖的获取配置文件地址的方法 */ protected String[] getConfigLocations() { String[] otherConfigs = getOtherConfigs(); String[] configFiles = new String[otherConfigs.length + 1]; // 所有配置文件列表 configFiles[0] = appContextFile; System.arraycopy(otherConfigs, 0, configFiles, 1, otherConfigs.length); return configFiles; } /** * 忽略的方法列表 * * @return */ abstract String[] getIgnoredMethods(); /** * 所有忽略方法列表中的方法都在测试时不执行 */ protected boolean isDisabledInThisEnvironment(String testMethodName) { for (String methodName : getIgnoredMethods()) { if (methodName.equals(testMethodName)) return true; } return false; } }
如果在测试时,需要将测试结果持久化,可以直接在测试的方法末尾加上“setComplete()”方法。由此,可以看出基于AbstractTransactionalSpringContextTests抽象基类的扩展,同样可以将数据持久化到数据库中,因此,实际测试中,可以使用此方式实现不启用自动事务回滚功能。针对需要测试的Service,编写的测试用例如下所示:
/** * TestDevCompanyServiceSpring.class */ package test.serviceTest.spring; import java.text.SimpleDateFormat; import java.util.Date; /** * 使用Spring测试机制的开发公司Service测试类<br/> * 自动事务回滚功能 * * @author cyq * */ public class TestDevCompanyServiceSpring extends AbstractServiceTransactionalTest { IDevCompanyService devCompanyService; // 需要测试的Service(自动注入) /** * */ public TestDevCompanyServiceSpring() { super(); } /** * @param name */ public TestDevCompanyServiceSpring(String name) { super(name); } /** * 当前需要加载的配置文件列表 */ String[] getOtherConfigs() { return new String[] { filePathSufix + "corp/devCompany-context.xml" }; } /** * 忽略方法列表 */ String[] getIgnoredMethods() { return new String[] { "testUpdateDevCompany", "testDeleteDevCompany" }; } /** * 测试添加开发公司的方法 */ public void testAddDevCompany() { System.out.println("add test..."); try { DevCompany entity = getDevCompanyService().addDevCompany( new DevCompany(new Long(1), "370101123456789098", "中国房地产开发总公司", new SimpleDateFormat("yyyy-MM-dd") .parse("1990-01-02"), "山东省济南市", new Long(10), new Date())); assertNotNull(entity); assertTrue(entity.getId() > 0); } catch (Exception e) { e.printStackTrace(); } // setComplete(); // 数据库记录不自动回滚 } /** * 测试查询方法 */ public void testQueryDevCompany() { System.out.println("query test..."); try { getDevCompanyService().queryDevCompanyByID(new Long(1)); Long counts = getDevCompanyService().queryDevCompanyCount(""); System.out.println("records size is:" + counts); assertTrue(counts > 0); getDevCompanyService().queryDevCompanyForList(null, 0, -1); } catch (Exception e) { e.printStackTrace(); } } /** * 测试更新方法 */ public void testUpdateDevCompany() { System.out.println("update test..."); try { DevCompany dc = getDevCompanyService().queryDevCompanyByID( new Long(1)); dc.setAddress(""); DevCompany entity = getDevCompanyService().updateDevCompany(dc); assertNotNull(entity); assertEquals("", entity.getAddress()); assertTrue("".equals(entity.getAddress())); } catch (Exception e) { e.printStackTrace(); } } /** * 测试删除方法 */ public void testDeleteDevCompany() { System.out.println("delete test..."); try { getDevCompanyService().deleteDevCompanyByID(new Long(1)); assertNull(getDevCompanyService().queryDevCompanyByID(new Long(1))); } catch (Exception e) { e.printStackTrace(); } } public IDevCompanyService getDevCompanyService() { return devCompanyService; } public void setDevCompanyService(IDevCompanyService devCompanyService) { this.devCompanyService = devCompanyService; } }
说明:
如果使用Spring的事务管理功能,此处在事务处理上可能会有一些冲突,例如将以“query”开头的方法的propagation设置为“NEVER”,测试时可能会造成事务自动回滚或者不回滚的异常,此时,需要做的操作是将以“query”开头的方法的propagation设置为默认的“REQUIRED”,同时加上read-only="true",可以解决此问题。
另外,AbstractTransactionalSpringContextTests提供了一些可以在事务处理过程中进行操作的各种入口。
onSetUpBeforeTransaction()与onTearDownAfterTransaction():子类可以覆盖这两个方法,可以在事务测试方法运行的前后执行一些数据库初始化的操作并在事务完成后将其清除;
onSetUpInTransaction()与onTearDownInTransaction():这对方法和上面介绍的方法完成的功能相同,只不过它们在测试方法的相同事务中执行的。
AbstractTransactionalSpringContextTests还提供了一组用于测试延迟数据加载的方法:endTransaction()与startNewTransaction()。当用户在测试Hibernate、JPA等允许延迟数据加载的应用时,模拟数据在Service层事务中被部分加载,当传递到Web层时重新打开事务完成延迟部分数据加载的测试场景。可以在测试方法中显式调用endTransaction()方法以模拟从Service层中获取部分数据后返回,然后,通过startNewTransaction()开启一个和原事务无关新事务——模拟在Web层中重新打开事务,接下来就可以访问延迟加载的数据,进行测试了。
基于AbstractTransactionalDataSourceSpringContextTests的扩展
AbstractTransactionalDataSourceSpringContextTests是目前Spring提供的测试机制中的终极方案,用户可以在此抽象类的子类中直接调用JdbcTemplate,自动使用Spring容器中的数据源(DataSource)创建好一个JdbcTemplate实例并开放给子类使用,方便用户进行JDBC相关操作,直接查看数据库中的数据是否正确。
对AbstractTransactionalDataSourceSpringContextTests做简单封装后的抽象类代码如下所示:
/** * AbstractJdbcServiceTestCase.class */ package test.serviceTest.spring; import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests; /** * @author cyq * */ public abstract class AbstractJdbcServiceTestCase extends AbstractTransactionalDataSourceSpringContextTests { String filePathSufix = "file:WebContent/WEB-INF/springConfig/"; // 配置文件地址的前缀 String appContextFile = filePathSufix + "applicationContext.xml";// applicationContext.xml文件地址 /** * */ public AbstractJdbcServiceTestCase() { super(); } /** * @param name */ public AbstractJdbcServiceTestCase(String name) { super(name); } /** * 需要加载的配置文件地址列表 * * @return */ abstract String[] getOtherConfigs(); /** * 覆盖的获取配置文件地址的方法 */ protected String[] getConfigLocations() { String[] otherConfigs = getOtherConfigs(); String[] configFiles = new String[otherConfigs.length + 1]; // 所有配置文件列表 configFiles[0] = appContextFile; System.arraycopy(otherConfigs, 0, configFiles, 1, otherConfigs.length); return configFiles; } /** * 忽略的方法列表 * * @return */ abstract String[] getIgnoredMethods(); /** * 所有忽略方法列表中的方法都在测试时不执行 */ protected boolean isDisabledInThisEnvironment(String testMethodName) { for (String methodName : getIgnoredMethods()) { if (methodName.equals(testMethodName)) return true; } return false; } }
此时在集成测试时,可以直接使用JdbcTemplate获取数据库中的值,从而验证方法是否正确执行,同时此时仍然可以保证数据库现场不被破坏,需要测试的Service其测试用例如下所示:
/** * TestDevCompanyServiceSpringJDBC.class */ package test.serviceTest.spring; import java.sql.ResultSet; import java.sql.SQLException; import java.text.SimpleDateFormat; import java.util.Date; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.ResultSetExtractor; /** * 使用Spring测试机制的开发公司Service测试类<br/> * 直接JDBC获取数据 * * @author cyq * */ public class TestDevCompanyServiceSpringJDBC extends AbstractJdbcServiceTestCase { IDevCompanyService devCompanyService; // 需要测试的Service /** * */ public TestDevCompanyServiceSpringJDBC() { super(); } /** * @param name */ public TestDevCompanyServiceSpringJDBC(String name) { super(name); } /** * 忽略的测试方法列表 */ String[] getIgnoredMethods() { return new String[] {}; } /** * 配置文件列表 */ String[] getOtherConfigs() { return new String[] { filePathSufix + "corp/devCompany-context.xml" }; } /** * 测试添加开发公司的方法 */ public void testAddDevCompany() { System.out.println("add test..."); try { DevCompany entity = getDevCompanyService().addDevCompany( new DevCompany(new Long(1), "370101123456789098", "中国房地产开发总公司", new SimpleDateFormat("yyyy-MM-dd") .parse("1990-01-02"), "山东省济南市", new Long(10), new Date())); assertNotNull(entity); assertTrue(entity.getId() > 0); Object result = getJdbcTemplate().query( "SELECT code FROM test_estate_c_devCompany WHERE id=1", new ResultSetExtractor() { public Object extractData(ResultSet rs) throws SQLException, DataAccessException { if (rs.next()) return rs.getString("code"); return null; } }); System.out.println("jdbc query result is:" + result.toString()); assertEquals("370101123456789098", result.toString()); } catch (Exception e) { e.printStackTrace(); } } public IDevCompanyService getDevCompanyService() { return devCompanyService; } public void setDevCompanyService(IDevCompanyService devCompanyService) { this.devCompanyService = devCompanyService; } }
使用JdbcTemplate直接获取数据库数据的方式,多用于更新以及复杂的业务逻辑中的数据验证。有了Spring的测试机制,对于使用Spring的应用,可以方便的进行集成测试,从而简化开发的工作量,减少繁琐的重复工作,提供工作效率。
注意:
如果采用byName的自动装配机制,数据源Bean的名称必须取名为“dataSource”。
另外,需要说明的是,Spring提供了另外一套在此基础上的基于注解的测试体系,用户在测试时直接使用注解可以更方便,而且使用注解还可以绕开由于事务的操作造成的某些问题(比如,本文提到的关于事务传播类型为“NEVER”的情况)。对于此,请参考另外一篇相关文章
。
相关文章推荐
- 使用Spring的测试机制进行集成测试
- Spring 使用注解方式进行事务管理--注解回滚机制
- Spring高级程序设计 21 使用Spring进行测试
- Mybatis集成Spring MVC,使用Spring test进行测试
- Spring整合Mongodb,Maven的依赖,Spring配置,MongoDB的公共操作类,使用SpringMVC的Controller进行测试并返回结果的案例
- Spring项目使用JUnit4进行测试
- 4000 Spring中用ApplicationContext进行测试的简单使用
- springboot项目中使用MockMvc 进行测试
- 使用spring机制进行单元测试
- 软件测试中使用HttpUnit进行集成测试(一)
- spring-mvc springboot 使用MockMvc对controller进行测试
- 使用Spring中Transcation进行数据库集成测试
- Spring下使用MockMvc类进行测试
- maven中使用spring的test包结合junit4进行测试。
- [j2ee][spring]使用UnitilsJUnit4进行测试
- Spring 集成测试1(对spring管理的bean进行单元测试,借助于spring提供的test case简化测试)
- Spring笔记(第三弹:使用JUnit对Spring工程进行测试)
- 利用Spring测试框架进行集成测试
- spring-boot整合mybatis(使用Fiddler抓包工具进行测试)
- 使用spring-mock进行dao集成测试