Spring testcontext
Spring 2.5 TestContext 测试框架用于测试基于 Spring 的程序,TestContext 测试框架和低版本 Spring 测试框架没有任何关系,是一个全新的基于注解的测试框架,为 Spring 推荐使用该测试框架。
- 基于注解的 IoC 功能;
- 基于注解驱动的 Spring MVC 功能;
- 基于注解的 TestContext 测试框架。
|
- 导致 Spring 容器多次初始化问题:根据 JUnit 测试用例的调用流程,每执行一个测试方法都会重新创建一个测试用例实例并调用其 setUp() 方法。由于在一般情况下,我们都在 setUp() 方法中初始化 Spring 容器,这意味着测试用例中有多少个测试方法,Spring 容器就会被重复初始化多少次。
- 需要使用硬编码方式手工获取 Bean:在测试用例中,我们需要通过 ApplicationContext.getBean() 的方法从 Spirng 容器中获取需要测试的目标 Bean,并且还要进行造型操作。
- 数据库现场容易遭受破坏:测试方法可能会对数据库记录进行更改操作,破坏数据库现场。虽然是针对开发数据库进行测试工作的,但如果数据操作的影响是持久的,将会形成积累效应并影响到测试用例的再次执行。举个例子,假设在某个测试方法中往数据库插入一条 ID 为 1 的 t_user 记录,第一次运行不会有问题,第二次运行时,就会因为主键冲突而导致测试用例执行失败。所以测试用例应该既能够完成测试固件业务功能正确性的检查,又能够容易地在测试完成后恢复现场,做到踏雪无迹、雁过无痕。
- 不容易在同一事务下访问数据库以检验业务操作的正确性:当测试固件操作数据库时,为了检测数据操作的正确性,需要通过一种方便途径在测试方法相同的事务环境下访问数据库,以检查测试固件数据操作的执行效果。如果直接使用 JUnit 进行测试,我们很难完成这项操作。
|
清单1. UserService.java 需要测试的服务类
package com.baobaotao.service; import com.baobaotao.domain.LoginLog; import com.baobaotao.domain.User; import com.baobaotao.dao.UserDao; import com.baobaotao.dao.LoginLogDao; public class UserService{ private UserDao userDao; private LoginLogDao loginLogDao; public void handleUserLogin(User user) { user.setCredits( 5 + user.getCredits()); LoginLog loginLog = new LoginLog(); loginLog.setUserId(user.getUserId()); loginLog.setIp(user.getLastIp()); loginLog.setLoginTime(user.getLastVisit()); userDao.updateLoginInfo(user); loginLogDao.insertLoginLog(loginLog); } //省略get/setter方法 } |
- 登录用户添加 5 个积分(t_user.credits);
- 登录用户的最后访问时间(t_user.last_visit)和 IP(t_user.last_ip)更新为当前值;
- 在日志表中(t_login_log)中为用户添加一条登录日志。
清单2. applicationContext.xml:Spring 配置文件,放在类路径下
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd"> <!-- 配置数据源 --> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close" p:driverClassName="com.mysql.jdbc.Driver" p:url="jdbc:mysql://localhost/sampledb" p:username="root" p:password="1234"/> <!-- 配置Jdbc模板 --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" p:dataSource-ref="dataSource"/> <!-- 配置dao --> <bean id="loginLogDao"class="com.baobaotao.dao.LoginLogDao" p:jdbcTemplate-ref="jdbcTemplate"/> <bean id="userDao" class="com.baobaotao.dao.UserDao" p:jdbcTemplate-ref="jdbcTemplate"/> <!-- 事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" p:dataSource-ref="dataSource"/> <bean id="userService" class="com.baobaotao.service.UserService" p:userDao-ref="userDao" p:loginLogDao-ref="loginLogDao"/> <!-- 使用aop/tx命名空间配置事务管理,这里对service包下的服务类方法提供事务--> <aop:config> <aop:pointcut id="jdbcServiceMethod" expression= "within(com.baobaotao.service..*)" /> <aop:advisor pointcut-ref="jdbcServiceMethod" advice-ref="jdbcTxAdvice" /> </aop:config> <tx:advice id="jdbcTxAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="*"/> </tx:attributes> </tx:advice> </beans> |
|
清单3.TestUserService.java: 基于注解的测试用例
package com.baobaotao.service; import org.springframework.test.context.junit4. AbstractTransactionalJUnit4SpringContextTests; import org.springframework.test.context.ContextConfiguration; import org.springframework.beans.factory.annotation.Autowired; import org.junit.Test; import com.baobaotao.domain.User; import java.util.Date; @ContextConfiguration //① public class TestUserService extends AbstractTransactionalJUnit4SpringContextTests { @Autowired //② private UserService userService; @Test //③ public void handleUserLogin(){ User user = new User(); user.setUserId(1); user.setLastIp("127.0.0.1"); Date now = new Date(); user.setLastVisit(now.getTime()); userService.handleUserLogin(user); } } |
-
locations:可以通过该属性手工指定 Spring 配置文件所在的位置,可以指定一个或多个 Spring 配置文件。如下所示:
@ContextConfiguration(locations={“xx/yy/beans1.xml”,” xx/yy/beans2.xml”})
-
inheritLocations:是否要继承父测试用例类中的 Spring 配置文件,默认为 true。如下面的例子:
@ContextConfiguration(locations={"base-context.xml"}) public class BaseTest { // ... } @ContextConfiguration(locations={"extended-context.xml"}) public class ExtendedTest extends BaseTest { // ... }
清单 4.TestUserService 所引用的 Spring 配置文件
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> <!-- ① 引入清单1定义的Spring配置文件 --> <import resource="classpath:/applicationContext.xml"/> </beans> |
图 1. 在 Eclipse 6.0 中运行 TestUserService
- 我们仅仅执行了 UserService#handleUserLogin(user) 方法,但验证该方法执行结果的正确性。
- 在测试方法中直接使用 ID 为 1 的 User 对象进行测试,这相当于要求在数据库 t_user 表必须已经存在 ID 为 1 的记录,如果 t_user 中不存在这条记录,将导致测试方法执行失败。
|
package com.baobaotao.service; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.Date; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.PreparedStatementCreator; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.KeyHolder; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4. AbstractTransactionalJUnit4SpringContextTests; import com.baobaotao.dao.UserDao; import com.baobaotao.domain.User; @ContextConfiguration public class TestUserService extends AbstractTransactionalJUnit4SpringContextTests { @Autowired private UserService userService; @Autowired private UserDao userDao; private int userId; @Before //① 准备测试数据 public void prepareTestData() { final String sql = "insert into t_user(user_name,password) values('tom','1234')"; simpleJdbcTemplate.update(sql); KeyHolder keyHolder = new GeneratedKeyHolder(); simpleJdbcTemplate.getJdbcOperations().update( new PreparedStatementCreator() { public PreparedStatement createPreparedStatement(Connection conn) throws SQLException { PreparedStatement ps = conn.prepareStatement(sql); return ps; } }, keyHolder); userId = keyHolder.getKey().intValue();//①-1 记录测试数据的id } @Test public void handleUserLogin(){ User user = userDao.getUserById(userId); //② 获取测试数据 user.setLastIp("127.0.0.1"); Date now = new Date(); user.setLastVisit(now.getTime()); userService.handleUserLogin(user); } } |
- protected int countRowsInTable(String tableName) :计算数据表的记录数。
- protected int deleteFromTables(String... names):删除表中的记录,可以指定多张表。
- protected void executeSqlScript(String sqlResourcePath, boolean continueOnError):执行 SQL 脚本文件,在脚本文件中,其格式必须一个 SQL 语句一行。
@Test public void handleUserLogin(){ User user = userDao.getUserById(userId); user.setLastIp("127.0.0.1"); Date now = new Date(); user.setLastVisit(now.getTime()); userService.handleUserLogin(user); //------------------以下为业务执行结果检查的代码--------------------- User newUser = userDao.getUserById(userId); Assert.assertEquals(5, newUser.getCredits()); //①检测积分 //①检测最后登录时间和IP Assert.assertEquals(now.getTime(), newUser.getLastVisit()); Assert.assertEquals("127.0.0.1",newUser.getLastIp()); // ③检测登录记录 String sql = "select count(1) from t_login_log where user_id=? "+ “ and login_datetime=? and ip=?"; int logCount =simpleJdbcTemplate.queryForInt(sql, user.getUserId(), user.getLastVisit(),user.getLastIp()); Assert.assertEquals(1, logCount); } |
|
图 2. Spring TestContext 测试框架核心类
- TestContext:它封装了运行测试用例的上下文;
- TestContextManager:它是进入 Spring TestContext 框架的程序主入口,它管理着一个 TestContext 实例,并在适合的执行点上向所有注册在 TestContextManager 中的 TestExecutionListener 监听器发布事件:比如测试用例实例的准备,测试方法执行前后方法的调用等。
- TestExecutionListener:该接口负责响应 TestContextManager 发布的事件。
@TestExecutionListeners( { DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class }) public class TestXxxService{ … } |
- DependencyInjectionTestExecutionListener:该监听器提供了自动注入的功能,它负责解析测试用例中 @Autowried 注解并完成自动注入;
- DirtiesContextTestExecutionListener:一般情况下测试方法并不会对 Spring 容器上下文造成破坏(改变 Bean 的配置信息等),如果某个测试方法确实会破坏 Spring 容器上下文,你可以显式地为该测试方法添加 @DirtiesContext 注解,以便 Spring TestContext 在测试该方法后刷新 Spring 容器的上下文,而 DirtiesContextTestExecutionListener 监听器的工作就是解析 @DirtiesContext 注解;
- TransactionalTestExecutionListener:它负责解析 @Transaction、@NotTransactional 以及 @Rollback 等事务注解的注解。@Transaction 注解让测试方法工作于事务环境中,不过在测试方法返回前事务会被回滚。你可以使用 @Rollback(false) 让测试方法返回前提交事务。而 @NotTransactional 注解则让测试方法不工作于事务环境中。此外,你还可以使用类或方法级别的 @TransactionConfiguration 注解改变事务管理策略,如下所示:
@TransactionConfiguration(transactionManager="txMgr", defaultRollback=false) @Transactional public class TestUserService { … }
@RunWith(SpringJUnit4ClassRunner.class) //① 指定测试用例运行器 @TestExecutionListeners( //② 注册了两个TestExecutionListener监听器 { DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class }) public class AbstractJUnit4SpringContextTests implements ApplicationContextAware { … } |
//① 注册测试用例事务管理的监听器 @TestExecutionListeners( { TransactionalTestExecutionListener.class }) @Transactional //② 使测试用例的所有方法都将工作于事务环境下 public class AbstractTransactionalJUnit4SpringContextTests extends AbstractJUnit4SpringContextTests { … } |
|