- 浏览: 128535 次
- 性别:
- 来自: 佛山
文章分类
最新评论
-
jackyrong:
hi,你好,能否发一份代码到我邮箱?谢谢了,jackyrong ...
基于struts2+spring+hibernate+jquery的jmesa分页实现样例 -
qdongl:
不错,继续学习
Java 通用数据库连接类[支持存储过程 参数自动识别] -
ww_o_ww:
学习了··哈哈·· ··
JAVA EXCEL API 使用 -
Angel_Night:
@TestExecutionListeners( { Tran ...
使用 Spring 2.5 TestContext 测试框架 -
ray111:
缺少DBConnectionManager类?
Java 通用数据库连接类[支持存储过程 参数自动识别]
Spring 2.5 TestContext 测试框架用于测试基于 Spring 的程序,TestContext 测试框架和低版本 Spring 测试框架没有任何关系,是一个全新的基于注解的测试框架,为 Spring 推荐使用该测试框架。
概述
Spring 2.5 相比于 Spring 2.0 所新增的最重要的功能可以归结为以下 3 点:
基于注解的 IoC 功能;
基于注解驱动的 Spring MVC 功能;
基于注解的 TestContext 测试框架。
Spring 推荐开发者使用新的基于注解的 TestContext 测试框架,本文我们将对此进行详细的讲述。
低版本的 Spring 所提供的 Spring 测试框架构在 JUnit 3.8 基础上扩展而来,它提供了若干个测试基类。而 Spring 2.5 所新增的基于注解的 TestContext 测试框架和低版本的测试框架没有任何关系。它采用全新的注解技术可以让 POJO 成为 Spring 的测试用例,除了拥有旧测试框架所有功能外,TestContext 还添加了一些新的功能,TestContext 可以运行在 JUnit 3.8、JUnit 4.4、TestNG 等测试框架下。
直接使用 JUnit 测试 Spring 程序存在的不足
在拙作《精通 Spring 2.x — 企业应用开发详解》一书中,笔者曾经指出如果直接使用 JUnit 测试基于 Spring 的程序,将存在以下 4 点明显的不足:
导致 Spring 容器多次初始化问题:根据 JUnit 测试用例的调用流程,每执行一个测试方法都会重新创建一个测试用例实例并调用其 setUp() 方法。由于在一般情况下,我们都在 setUp() 方法中初始化 Spring 容器,这意味着测试用例中有多少个测试方法,Spring 容器就会被重复初始化多少次。
需要使用硬编码方式手工获取 Bean:在测试用例中,我们需要通过 ApplicationContext.getBean() 的方法从 Spirng 容器中获取需要测试的目标 Bean,并且还要进行造型操作。
数据库现场容易遭受破坏:测试方法可能会对数据库记录进行更改操作,破坏数据库现场。虽然是针对开发数据库进行测试工作的,但如果数据操作的影响是持久的,将会形成积累效应并影响到测试用例的再次执行。举个例子,假设在某个测试方法中往数据库插入一条 ID 为 1 的 t_user 记录,第一次运行不会有问题,第二次运行时,就会因为主键冲突而导致测试用例执行失败。所以测试用例应该既能够完成测试固件业务功能正确性的检查,又能够容易地在测试完成后恢复现场,做到踏雪无迹、雁过无痕。
不容易在同一事务下访问数据库以检验业务操作的正确性:当测试固件操作数据库时,为了检测数据操作的正确性,需要通过一种方便途径在测试方法相同的事务环境下访问数据库,以检查测试固件数据操作的执行效果。如果直接使用 JUnit 进行测试,我们很难完成这项操作。
Spring 测试框架是专门为测试基于 Spring 框架应用程序而设计的,它能够让测试用例非常方便地和 Spring 框架结合起来,以上所有问题都将迎刃而解。
一个需要测试的 Spring 服务类
在具体使用 TextContext 测试框架之前,我们先来认识一下需要测试的 UserService 服务类。UserService 服务类中拥有一个处理用户登录的服务方法,其代码如下所示:
UserService 需要调用 DAO 层的 UserDao 和 LoginLogDao 以及 User 和 LoginLog 这两个 PO 完成业务逻辑,User 和 LoginLog分别对应 t_user 和 t_login_log 这两张数据库表。
在用户登录成功后调用 UserService 中的 handleUserLogin() 方法执行用户登录成功后的业务逻辑:
登录用户添加 5 个积分(t_user.credits);
登录用户的最后访问时间(t_user.last_visit)和 IP(t_user.last_ip)更新为当前值;
在日志表中(t_login_log)中为用户添加一条登录日志。
这是一个需要访问数据库并存在数据更改操作的业务方法,它工作在事务环境下。下面是装配该服务类 Bean 的 Spring 配置文件:
UserService 所关联的 DAO 类和 PO 类都比较简单,请参看本文附件的程序代码。在着手测试 UserSerivce 之前,需要将创建数据库表,你可以在附件的 schema 目录下找到相应的 SQL 脚本文件。
编写 UserService 的测试用例
下面我们为 UserService 编写一个简单的测试用例类,此时的目标是让这个基于 TestContext 测试框架的测试类运行起来,我们将在后面逐步完善这个测试用例。
TestUserService.java: 基于注解的测试用例
这里,我们让 TestUserService 直接继承于 Spring 所提供的 AbstractTransactionalJUnit4SpringContextTests 的抽象测试类,稍后本文将对这个抽象测试类进行剖析,这里你仅须知道该抽象测试类的作用是让 TestContext 测试框架可以在 JUnit 4.4 测试框架基础上运行起来就可以了。
在 ① 处,标注了一个类级的 @ContextConfiguration 注解,这里 Spring 将按 TestContext 契约查找 classpath:/com/baobaotao/service/TestUserService-context.xml 的 Spring 配置文件,并使用该配置文件启动 Spring 容器。@ContextConfiguration 注解有以下两个常用的属性:
locations:可以通过该属性手工指定 Spring 配置文件所在的位置,可以指定一个或多个 Spring 配置文件。如下所示:
inheritLocations:是否要继承父测试用例类中的 Spring 配置文件,默认为 true。如下面的例子:
如果 inheritLocations 设置为 false,则 ExtendedTest 仅会使用 extended-context.xml 配置文件,否则将使用 base-context.xml 和 extended-context.xml 这两个配置文件。
② 处的 @Autowired 注解让 Spring 容器自动注入 UserService 类型的 Bean。而在 ③ 处标注的 @Test 注解则让 handleUserLogin() 方法成为一个 JUnit 4.4 标准的测试方法, @Test 是 JUnit 4.4 所定义的注解。
在运行 TestUserService 测试类之前,让我们先看一下 TestUserService-context.xml 配置文件的内容:
TestUserService 所引用的 Spring 配置文件
在 ① 处引入了清单 1 中定义的 Spring 配置文件,这样我们就可以将其中定义的 UserService Bean 作为测试固件注入到 TestUserService 中了。
在你的 IDE 中(Eclipse、JBuilder、Idea 等),将 JUnit 4.4 类包引入到项目工程中后,在 TestUserService 类中点击右键运行该测试类,将发现 TestUserService 已经可以成功运行了,如 图 1 所示:
图 1. 在 Eclipse 6.0 中运行 TestUserService
TestUserService 可以正确运行,说明其 userService 这个测试固件已经享受了 Spring 自动注入的功能。在运行该测试用例后,到数据库中查看 t_user 表和 t_login_log 表,你会发现表数据和测试前是一样的!这说明虽然我们在清单 3 的 handleUserLogin() 测试方法中执行了 userService.handleUserLogin(user) 的操作,但它并没有对数据库现场造成破坏:这是因为 Spring 的在测试方法返回前进行了事务回滚操作。
虽然 TestUserService.handleUserLogin() 测试方法已经可以成功运行,但是它在测试功能上是不完善的,读者朋友可以已经发现了它存在以下两个问题:
我们仅仅执行了 UserService#handleUserLogin(user) 方法,但验证该方法执行结果的正确性。
在测试方法中直接使用 ID 为 1 的 User 对象进行测试,这相当于要求在数据库 t_user 表必须已经存在 ID 为 1 的记录,如果 t_user 中不存在这条记录,将导致测试方法执行失败。
准备测试数据并检测运行结果
在这节里,我们将着手解决上面所提出的两个问题,在测试用例中准备测试数据并到数据库中检测业务执行结果的正确性。
准备测试数据
相比于在测试方法中直接访问预定的数据记录,在测试方法执行前通过程序准备一些测试数据,然后在此基础上运行测试方法是比较好的策略,因为后者不需要对数据库的状态做假设。在 TestContext 中,你可以通过使用 JUnit 4.4 的 @Before 注解达到这个目的,请看下面的代码:
为测试方法准备数据
JUnit 4.4 允许通过注解指定某些方法在测试方法执行前后进行调用,即是 @Before 和 @After 注解。在 Spring TestContext 中,标注 @Before 和 @After 的方法会在测试用例中每个测试方法运行前后执行,并和测试方法运行于同一个事务中。在 清单 5 中 ① 处,我们给 prepareTestData() 标注上了 @Before 注解,在该方法中准备一些测试数据,以供 TestUserService 中所有测试方法使用(这里仅有一个 handleUserLogin() 测试方法)。由于测试方法运行后,整个事务会被回滚,在 prepareTestData() 中插入的测试数据也不会持久化到数据库中,因此我们无须手工删除这条记录。
标注 @Before 或 @After 注解的方法和测试方法运行在同一个事务中,但有时我们希望在测试方法的事务开始之前或完成之后执行某些方法以便获取数据库现场的一些情况。这时,可以使用 Spring TestContext 的 @BeforeTransaction 和 @AfterTransaction 注解来达到目录(这两个注解位于 org.springframework.test.context.transaction 包中)。
虽然大多数业务方法都会访问数据库,但也并非所有需要测试的业务方法都需要和数据库打交道。而在默认情况下,继承于 AbstractTransactionalJUnit4SpringContextTests 测试用例的所有测试方法都将工作于事务环境下,你可以显式地通过 @NotTransactional 注解,让测试方法不工作于事务环境下。
prepareTestData() 方法中使用到了 simpleJdbcTemplate 对象访问操作数据库,该对象在 AbstractTransactionalJUnit4SpringContextTests 抽象类中定义,只要 Spring 容器有配置数据源,simpleJdbcTemplate 就会被自动创建。同时该抽象类中还拥有一个 Spring 容器引用:applicationContext,你可以借助该成员变量访问 Spring 容器,执行获取 Bean,发布事件等操作。
此外,AbstractTransactionalJUnit4SpringContextTests 还提供了若干个访问数据库的便捷方法,说明如下:
检验业务逻辑的正确性
到目前为此,TestUserService 的 handleUserLogin() 测试方法仅是简单地执行 UserService#handleUserLogin() 业务方法,但并没有在业务方法执行后检查执行结果的正确性,因此这个测试是不到位的。也就是说,我们必须访问数据库以检查业务方法对数据更改是否成功:这包括积分(credits)、最后登录时间(last_visit)、最后登录 IP(last_ip)以及登录日志表中的登录日志记录(t_login_log)。下面,我们补充这项重要的检查数据正确性的工作:
检验业务方法执行结果的正确性
在业务方法执行后,我们查询数据库中相应记录以检查是否和期望的效果一致,如 ① 和 ② 所示。在 ③ 处,我们使用 SimpleJdbcTemplate 查询 t_login_log,以检查该表中是否已经添加了一条用户登录日志。
注意:由于我们的 DAO 层采用 Spring JDBC 框架,它没有采用服务层缓存技术,所以可以使用 DAO 类返回数据库中的数据。如果采用 Hibernate 等 ORM 框架,由于它们采用了服务层缓存的技术,为了获取数据库中的相应数据,需要在业务方法执行后调用 HibernateTemplate.flush() 方法,将缓存中的对象同步到数据库中,这时才可以通过 SimpleJdbcTemplate 在数据库中访问业务方法的执行情况。
Spring TestContext 测试框架体系结构
在前面,我们直接通过扩展 AbstractTransactionalJUnit4SpringContextTests 编写测试用例,在了解了编写基于 TestContext 测试框架的测试用例后,现在是了解 TestContext 测试框架本身的时候了。
TestContext 核心类、支持类以及注解类
TestContext 测试框架的核心由 org.springframework.test.context 包中三个类组成,分别是 TestContext 和 TestContextManager 类以及 TestExecutionListener
TestContext:它封装了运行测试用例的上下文;
TestContextManager:它是进入 Spring TestContext 框架的程序主入口,它管理着一个 TestContext 实例,并在适合的执行点上向所有注册在 TestContextManager 中的 TestExecutionListener 监听器发布事件:比如测试用例实例的准备,测试方法执行前后方法的调用等。
TestExecutionListener:该接口负责响应 TestContextManager 发布的事件。
Spring TestContext 允许在测试用例类中通过 @TestExecutionListeners 注解向 TestContextManager 注册多个监听器,如下所示:
Spring 提供了几个 TestExecutionListener 接口实现类,分别说明如下:
DependencyInjectionTestExecutionListener:该监听器提供了自动注入的功能,它负责解析测试用例中 @Autowried 注解并完成自动注入;
DirtiesContextTestExecutionListener:一般情况下测试方法并不会对 Spring 容器上下文造成破坏(改变 Bean 的配置信息等),如果某个测试方法确实会破坏 Spring 容器上下文,你可以显式地为该测试方法添加 @DirtiesContext 注解,以便 Spring TestContext 在测试该方法后刷新 Spring 容器的上下文,而 DirtiesContextTestExecutionListener 监听器的工作就是解析 @DirtiesContext 注解;
TransactionalTestExecutionListener:它负责解析 @Transaction、@NotTransactional 以及 @Rollback 等事务注解的注解。@Transaction 注解让测试方法工作于事务环境中,不过在测试方法返回前事务会被回滚。你可以使用 @Rollback(false) 让测试方法返回前提交事务。而 @NotTransactional 注解则让测试方法不工作于事务环境中。此外,你还可以使用类或方法级别的 @TransactionConfiguration 注解改变事务管理策略,如下所示:
我们知道在 JUnit 4.4 中可以通过 @RunWith 注解指定测试用例的运行器,Spring TestContext 框架提供了扩展于 org.junit.internal.runners.JUnit4ClassRunner 的 SpringJUnit4ClassRunner 运行器,它负责总装 Spring TestContext 测试框架并将其统一到 JUnit 4.4 框架中。
TestContext 所提供的抽象测试用例
Spring TestContext 为基于 JUnit 4.4 测试框架提供了两个抽象测试用例类,分别是 AbstractJUnit4SpringContextTests 和 AbstractTransactionalJUnit4SpringContextTests,而后者扩展于前者。让我们来看一下这两个抽象测试用例类的骨架代码:
① 处将 SpringJUnit4ClassRunner 指定为测试用例运行器,它负责无缝地将 TestContext 测试框架移花接木到 JUnit 4.4 测试框架中,它是 Spring TestContext 可以运行起来的根本所在。② 处通过 @TestExecutionListeners 注解向测试用例类中注册了两个 TestExecutionListener 监听器,这两个监听器分别负责对 @Autowired 和 @DirtiesContext 注解进行处理,为测试用例提供自动注入和重新刷新 Spring 容器上下文的功能。
AbstractTransactionalJUnit4SpringContextTests 扩展于 AbstractJUnit4SpringContextTests,提供了事务管理的支持,其骨架代码如下所示:
在 ① 处,AbstractTransactionalJUnit4SpringContextTests 向测试用例类中注册了 TransactionalTestExecutionListener 监听器,这样测试用例中的 @Transaction、@NotTransaction 以及 @Rollback 等注解就可以正确地工作起来了。注意,你不需要在 Spring 配置文件通过 <tx:annotation-driven /> 和 <context:annotation-config/> 为测试用例类启用注解事务驱动和注解自动注入,这个工作完全于 TestContext 自身来解决(通过注册 DependencyInjectionTestExecutionListener 和 TransactionalTestExecutionListener 监听器),毕竟测试用例类没有注册到 Spring 容器中,没有成为 Spring 的 Bean。
小结
我们通过对一个典型的涉及数据库访问操作的 UserService 服务类的测试,讲述了使用 Spring 2.5 TestContext 测试框架进行集成测试的各项问题,这包括测试固件的自动注入、事务自动回滚、通过 SimpleJdbcTemplate 直接访问数据库以及测试数据准备等问题。
在通过一个实际例子的学习后,我们对如何使用 TestContext 测试框架有了一个具体的认识,在此基础上我们对 Spring TestContext 测试框架体系结构进行了分析,然后剖析了 Spring 为 TestContext 嫁接到 JUnit 4.4 测试框架上所提供的两个抽象测试用例类。
Spring 的 TestContext 测试框架不但可以整合到 JUnit 4.4 测试框架上,而且还可以整合到 JUnit 3.8 以及 TestNG 等测试框架上。目前已经提供了对 JUnit 3.8 以及 TestNG 的支持,你可以分别在 org.springframework.test.context.junit38 和 org.springframework.test.context.testng 包下找到整合的帮助类。
概述
Spring 2.5 相比于 Spring 2.0 所新增的最重要的功能可以归结为以下 3 点:
基于注解的 IoC 功能;
基于注解驱动的 Spring MVC 功能;
基于注解的 TestContext 测试框架。
Spring 推荐开发者使用新的基于注解的 TestContext 测试框架,本文我们将对此进行详细的讲述。
低版本的 Spring 所提供的 Spring 测试框架构在 JUnit 3.8 基础上扩展而来,它提供了若干个测试基类。而 Spring 2.5 所新增的基于注解的 TestContext 测试框架和低版本的测试框架没有任何关系。它采用全新的注解技术可以让 POJO 成为 Spring 的测试用例,除了拥有旧测试框架所有功能外,TestContext 还添加了一些新的功能,TestContext 可以运行在 JUnit 3.8、JUnit 4.4、TestNG 等测试框架下。
直接使用 JUnit 测试 Spring 程序存在的不足
在拙作《精通 Spring 2.x — 企业应用开发详解》一书中,笔者曾经指出如果直接使用 JUnit 测试基于 Spring 的程序,将存在以下 4 点明显的不足:
导致 Spring 容器多次初始化问题:根据 JUnit 测试用例的调用流程,每执行一个测试方法都会重新创建一个测试用例实例并调用其 setUp() 方法。由于在一般情况下,我们都在 setUp() 方法中初始化 Spring 容器,这意味着测试用例中有多少个测试方法,Spring 容器就会被重复初始化多少次。
需要使用硬编码方式手工获取 Bean:在测试用例中,我们需要通过 ApplicationContext.getBean() 的方法从 Spirng 容器中获取需要测试的目标 Bean,并且还要进行造型操作。
数据库现场容易遭受破坏:测试方法可能会对数据库记录进行更改操作,破坏数据库现场。虽然是针对开发数据库进行测试工作的,但如果数据操作的影响是持久的,将会形成积累效应并影响到测试用例的再次执行。举个例子,假设在某个测试方法中往数据库插入一条 ID 为 1 的 t_user 记录,第一次运行不会有问题,第二次运行时,就会因为主键冲突而导致测试用例执行失败。所以测试用例应该既能够完成测试固件业务功能正确性的检查,又能够容易地在测试完成后恢复现场,做到踏雪无迹、雁过无痕。
不容易在同一事务下访问数据库以检验业务操作的正确性:当测试固件操作数据库时,为了检测数据操作的正确性,需要通过一种方便途径在测试方法相同的事务环境下访问数据库,以检查测试固件数据操作的执行效果。如果直接使用 JUnit 进行测试,我们很难完成这项操作。
Spring 测试框架是专门为测试基于 Spring 框架应用程序而设计的,它能够让测试用例非常方便地和 Spring 框架结合起来,以上所有问题都将迎刃而解。
一个需要测试的 Spring 服务类
在具体使用 TextContext 测试框架之前,我们先来认识一下需要测试的 UserService 服务类。UserService 服务类中拥有一个处理用户登录的服务方法,其代码如下所示:
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方法 }
UserService 需要调用 DAO 层的 UserDao 和 LoginLogDao 以及 User 和 LoginLog 这两个 PO 完成业务逻辑,User 和 LoginLog分别对应 t_user 和 t_login_log 这两张数据库表。
在用户登录成功后调用 UserService 中的 handleUserLogin() 方法执行用户登录成功后的业务逻辑:
登录用户添加 5 个积分(t_user.credits);
登录用户的最后访问时间(t_user.last_visit)和 IP(t_user.last_ip)更新为当前值;
在日志表中(t_login_log)中为用户添加一条登录日志。
这是一个需要访问数据库并存在数据更改操作的业务方法,它工作在事务环境下。下面是装配该服务类 Bean 的 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>
UserService 所关联的 DAO 类和 PO 类都比较简单,请参看本文附件的程序代码。在着手测试 UserSerivce 之前,需要将创建数据库表,你可以在附件的 schema 目录下找到相应的 SQL 脚本文件。
编写 UserService 的测试用例
下面我们为 UserService 编写一个简单的测试用例类,此时的目标是让这个基于 TestContext 测试框架的测试类运行起来,我们将在后面逐步完善这个测试用例。
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); } }
这里,我们让 TestUserService 直接继承于 Spring 所提供的 AbstractTransactionalJUnit4SpringContextTests 的抽象测试类,稍后本文将对这个抽象测试类进行剖析,这里你仅须知道该抽象测试类的作用是让 TestContext 测试框架可以在 JUnit 4.4 测试框架基础上运行起来就可以了。
在 ① 处,标注了一个类级的 @ContextConfiguration 注解,这里 Spring 将按 TestContext 契约查找 classpath:/com/baobaotao/service/TestUserService-context.xml 的 Spring 配置文件,并使用该配置文件启动 Spring 容器。@ContextConfiguration 注解有以下两个常用的属性:
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 { // ... }
如果 inheritLocations 设置为 false,则 ExtendedTest 仅会使用 extended-context.xml 配置文件,否则将使用 base-context.xml 和 extended-context.xml 这两个配置文件。
② 处的 @Autowired 注解让 Spring 容器自动注入 UserService 类型的 Bean。而在 ③ 处标注的 @Test 注解则让 handleUserLogin() 方法成为一个 JUnit 4.4 标准的测试方法, @Test 是 JUnit 4.4 所定义的注解。
在运行 TestUserService 测试类之前,让我们先看一下 TestUserService-context.xml 配置文件的内容:
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 中定义的 Spring 配置文件,这样我们就可以将其中定义的 UserService Bean 作为测试固件注入到 TestUserService 中了。
在你的 IDE 中(Eclipse、JBuilder、Idea 等),将 JUnit 4.4 类包引入到项目工程中后,在 TestUserService 类中点击右键运行该测试类,将发现 TestUserService 已经可以成功运行了,如 图 1 所示:
图 1. 在 Eclipse 6.0 中运行 TestUserService
TestUserService 可以正确运行,说明其 userService 这个测试固件已经享受了 Spring 自动注入的功能。在运行该测试用例后,到数据库中查看 t_user 表和 t_login_log 表,你会发现表数据和测试前是一样的!这说明虽然我们在清单 3 的 handleUserLogin() 测试方法中执行了 userService.handleUserLogin(user) 的操作,但它并没有对数据库现场造成破坏:这是因为 Spring 的在测试方法返回前进行了事务回滚操作。
虽然 TestUserService.handleUserLogin() 测试方法已经可以成功运行,但是它在测试功能上是不完善的,读者朋友可以已经发现了它存在以下两个问题:
我们仅仅执行了 UserService#handleUserLogin(user) 方法,但验证该方法执行结果的正确性。
在测试方法中直接使用 ID 为 1 的 User 对象进行测试,这相当于要求在数据库 t_user 表必须已经存在 ID 为 1 的记录,如果 t_user 中不存在这条记录,将导致测试方法执行失败。
准备测试数据并检测运行结果
在这节里,我们将着手解决上面所提出的两个问题,在测试用例中准备测试数据并到数据库中检测业务执行结果的正确性。
准备测试数据
相比于在测试方法中直接访问预定的数据记录,在测试方法执行前通过程序准备一些测试数据,然后在此基础上运行测试方法是比较好的策略,因为后者不需要对数据库的状态做假设。在 TestContext 中,你可以通过使用 JUnit 4.4 的 @Before 注解达到这个目的,请看下面的代码:
为测试方法准备数据
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); } }
JUnit 4.4 允许通过注解指定某些方法在测试方法执行前后进行调用,即是 @Before 和 @After 注解。在 Spring TestContext 中,标注 @Before 和 @After 的方法会在测试用例中每个测试方法运行前后执行,并和测试方法运行于同一个事务中。在 清单 5 中 ① 处,我们给 prepareTestData() 标注上了 @Before 注解,在该方法中准备一些测试数据,以供 TestUserService 中所有测试方法使用(这里仅有一个 handleUserLogin() 测试方法)。由于测试方法运行后,整个事务会被回滚,在 prepareTestData() 中插入的测试数据也不会持久化到数据库中,因此我们无须手工删除这条记录。
标注 @Before 或 @After 注解的方法和测试方法运行在同一个事务中,但有时我们希望在测试方法的事务开始之前或完成之后执行某些方法以便获取数据库现场的一些情况。这时,可以使用 Spring TestContext 的 @BeforeTransaction 和 @AfterTransaction 注解来达到目录(这两个注解位于 org.springframework.test.context.transaction 包中)。
虽然大多数业务方法都会访问数据库,但也并非所有需要测试的业务方法都需要和数据库打交道。而在默认情况下,继承于 AbstractTransactionalJUnit4SpringContextTests 测试用例的所有测试方法都将工作于事务环境下,你可以显式地通过 @NotTransactional 注解,让测试方法不工作于事务环境下。
prepareTestData() 方法中使用到了 simpleJdbcTemplate 对象访问操作数据库,该对象在 AbstractTransactionalJUnit4SpringContextTests 抽象类中定义,只要 Spring 容器有配置数据源,simpleJdbcTemplate 就会被自动创建。同时该抽象类中还拥有一个 Spring 容器引用:applicationContext,你可以借助该成员变量访问 Spring 容器,执行获取 Bean,发布事件等操作。
此外,AbstractTransactionalJUnit4SpringContextTests 还提供了若干个访问数据库的便捷方法,说明如下:
引用
protected int countRowsInTable(String tableName) :计算数据表的记录数。
protected int deleteFromTables(String... names):删除表中的记录,可以指定多张表。
protected void executeSqlScript(String sqlResourcePath, boolean continueOnError):执行 SQL 脚本文件,在脚本文件中,其格式必须一个 SQL 语句一行。
在测试方法 handleUserLogin() 的 ② 处,我们通过 userDao 获取 prepareTestData() 添加的测试数据,测试方法在测试数据的基础上执行业务逻辑。使用这种测试方式后,在任何情况下运行 TestUserService 都不会发生业务逻辑之外的问题。
protected int deleteFromTables(String... names):删除表中的记录,可以指定多张表。
protected void executeSqlScript(String sqlResourcePath, boolean continueOnError):执行 SQL 脚本文件,在脚本文件中,其格式必须一个 SQL 语句一行。
在测试方法 handleUserLogin() 的 ② 处,我们通过 userDao 获取 prepareTestData() 添加的测试数据,测试方法在测试数据的基础上执行业务逻辑。使用这种测试方式后,在任何情况下运行 TestUserService 都不会发生业务逻辑之外的问题。
检验业务逻辑的正确性
到目前为此,TestUserService 的 handleUserLogin() 测试方法仅是简单地执行 UserService#handleUserLogin() 业务方法,但并没有在业务方法执行后检查执行结果的正确性,因此这个测试是不到位的。也就是说,我们必须访问数据库以检查业务方法对数据更改是否成功:这包括积分(credits)、最后登录时间(last_visit)、最后登录 IP(last_ip)以及登录日志表中的登录日志记录(t_login_log)。下面,我们补充这项重要的检查数据正确性的工作:
检验业务方法执行结果的正确性
@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); }
在业务方法执行后,我们查询数据库中相应记录以检查是否和期望的效果一致,如 ① 和 ② 所示。在 ③ 处,我们使用 SimpleJdbcTemplate 查询 t_login_log,以检查该表中是否已经添加了一条用户登录日志。
注意:由于我们的 DAO 层采用 Spring JDBC 框架,它没有采用服务层缓存技术,所以可以使用 DAO 类返回数据库中的数据。如果采用 Hibernate 等 ORM 框架,由于它们采用了服务层缓存的技术,为了获取数据库中的相应数据,需要在业务方法执行后调用 HibernateTemplate.flush() 方法,将缓存中的对象同步到数据库中,这时才可以通过 SimpleJdbcTemplate 在数据库中访问业务方法的执行情况。
Spring TestContext 测试框架体系结构
在前面,我们直接通过扩展 AbstractTransactionalJUnit4SpringContextTests 编写测试用例,在了解了编写基于 TestContext 测试框架的测试用例后,现在是了解 TestContext 测试框架本身的时候了。
TestContext 核心类、支持类以及注解类
TestContext 测试框架的核心由 org.springframework.test.context 包中三个类组成,分别是 TestContext 和 TestContextManager 类以及 TestExecutionListener
TestContext:它封装了运行测试用例的上下文;
TestContextManager:它是进入 Spring TestContext 框架的程序主入口,它管理着一个 TestContext 实例,并在适合的执行点上向所有注册在 TestContextManager 中的 TestExecutionListener 监听器发布事件:比如测试用例实例的准备,测试方法执行前后方法的调用等。
TestExecutionListener:该接口负责响应 TestContextManager 发布的事件。
Spring TestContext 允许在测试用例类中通过 @TestExecutionListeners 注解向 TestContextManager 注册多个监听器,如下所示:
@TestExecutionListeners( { DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class }) public class TestXxxService{ … }
Spring 提供了几个 TestExecutionListener 接口实现类,分别说明如下:
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 { … }
我们知道在 JUnit 4.4 中可以通过 @RunWith 注解指定测试用例的运行器,Spring TestContext 框架提供了扩展于 org.junit.internal.runners.JUnit4ClassRunner 的 SpringJUnit4ClassRunner 运行器,它负责总装 Spring TestContext 测试框架并将其统一到 JUnit 4.4 框架中。
TestContext 所提供的抽象测试用例
Spring TestContext 为基于 JUnit 4.4 测试框架提供了两个抽象测试用例类,分别是 AbstractJUnit4SpringContextTests 和 AbstractTransactionalJUnit4SpringContextTests,而后者扩展于前者。让我们来看一下这两个抽象测试用例类的骨架代码:
@RunWith(SpringJUnit4ClassRunner.class) //① 指定测试用例运行器 @TestExecutionListeners( //② 注册了两个TestExecutionListener监听器 { DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class }) public class AbstractJUnit4SpringContextTests implements ApplicationContextAware { … }
① 处将 SpringJUnit4ClassRunner 指定为测试用例运行器,它负责无缝地将 TestContext 测试框架移花接木到 JUnit 4.4 测试框架中,它是 Spring TestContext 可以运行起来的根本所在。② 处通过 @TestExecutionListeners 注解向测试用例类中注册了两个 TestExecutionListener 监听器,这两个监听器分别负责对 @Autowired 和 @DirtiesContext 注解进行处理,为测试用例提供自动注入和重新刷新 Spring 容器上下文的功能。
AbstractTransactionalJUnit4SpringContextTests 扩展于 AbstractJUnit4SpringContextTests,提供了事务管理的支持,其骨架代码如下所示:
//① 注册测试用例事务管理的监听器 @TestExecutionListeners( { TransactionalTestExecutionListener.class }) @Transactional //② 使测试用例的所有方法都将工作于事务环境下 public class AbstractTransactionalJUnit4SpringContextTests extends AbstractJUnit4SpringContextTests { … }
在 ① 处,AbstractTransactionalJUnit4SpringContextTests 向测试用例类中注册了 TransactionalTestExecutionListener 监听器,这样测试用例中的 @Transaction、@NotTransaction 以及 @Rollback 等注解就可以正确地工作起来了。注意,你不需要在 Spring 配置文件通过 <tx:annotation-driven /> 和 <context:annotation-config/> 为测试用例类启用注解事务驱动和注解自动注入,这个工作完全于 TestContext 自身来解决(通过注册 DependencyInjectionTestExecutionListener 和 TransactionalTestExecutionListener 监听器),毕竟测试用例类没有注册到 Spring 容器中,没有成为 Spring 的 Bean。
小结
我们通过对一个典型的涉及数据库访问操作的 UserService 服务类的测试,讲述了使用 Spring 2.5 TestContext 测试框架进行集成测试的各项问题,这包括测试固件的自动注入、事务自动回滚、通过 SimpleJdbcTemplate 直接访问数据库以及测试数据准备等问题。
在通过一个实际例子的学习后,我们对如何使用 TestContext 测试框架有了一个具体的认识,在此基础上我们对 Spring TestContext 测试框架体系结构进行了分析,然后剖析了 Spring 为 TestContext 嫁接到 JUnit 4.4 测试框架上所提供的两个抽象测试用例类。
Spring 的 TestContext 测试框架不但可以整合到 JUnit 4.4 测试框架上,而且还可以整合到 JUnit 3.8 以及 TestNG 等测试框架上。目前已经提供了对 JUnit 3.8 以及 TestNG 的支持,你可以分别在 org.springframework.test.context.junit38 和 org.springframework.test.context.testng 包下找到整合的帮助类。
评论
1 楼
Angel_Night
2011-06-03
@TestExecutionListeners( { TransactionalTestExecutionListener.class })
解决了 我一直不能@Transactional的问题 非常感谢
解决了 我一直不能@Transactional的问题 非常感谢
发表评论
-
应用设计模式编写易于单元测试的代码
2008-07-24 15:14 929单元测试是软件开发的一个重要组成部分,通过在软件设计、 ... -
怎样使用Junit Framework进行单元测试的编写
2008-07-24 15:10 1684您的传统代码是不 ... -
单元测试利器 JUnit 4
2008-07-24 14:44 2198本文主要介绍了如 ... -
TestNG 使 Java 单元测试轻而易举
2008-07-24 13:31 1433JUnit 框架是 Java 语言单 ... -
再讨论spring的单元测试
2008-07-01 22:31 1025在我以前一篇文章关于s ... -
spring中的单元测试的策略
2008-07-01 22:30 1588本文主要介绍使用spring提供的对junit的扩展机制来进行 ...
相关推荐
- 虽然Spock框架在Spring 2.5之后才逐渐流行,但通过适配器,也可以与Spring TestContext框架一起使用,提供更丰富的测试语法和功能。 9. **Web应用程序测试**: - Spring 2.5 TestContext框架也支持Web应用程序...
TestContext框架是Spring测试支持的一部分,它提供了一个统一的测试上下文,可以加载Spring配置,并为测试注入依赖。以下是对Spring测试框架及其核心组件的详细说明: 1. **测试注解**:Spring测试框架利用注解简化...
使用 Spring 2.5 TestContext 测试框架编写测试用例的基本步骤如下: 1. **导入必要的依赖**:首先确保项目中包含 Spring 测试模块的相关依赖。 2. **定义测试类**:测试类通常需要继承自 `...
5. **测试**:创建一个JUnit测试类,使用Spring的TestContext框架来加载配置并运行测试。确保你的测试类继承了`AbstractJUnit4SpringContextTests`或`AbstractTestNGSpringContextTests`(取决于你使用的测试框架)...
Spring 2.5提供了一套完整的测试框架,包括单元测试和集成测试工具,如MockMVC和TestContext框架,使得测试Spring应用变得更加便捷。 9. **Web服务支持** Spring 2.5加强了对Web服务的支持,包括SOAP和RESTful...
10. **测试支持**:Spring 2.5 强化了测试框架,包括 `MockMVC` 和 `TestContext` 框架,使得单元测试和集成测试更加方便。 以上是 Spring 2.5 架构中的核心知识点,它们共同构建了一个强大且灵活的应用开发框架,...
10. **单元测试和集成测试支持**:Spring提供了TestContext框架,使得编写和运行单元测试以及集成测试变得简单。 通过阅读这本中文参考手册,开发者可以深入了解Spring 2.5的各种功能,掌握如何使用Spring构建高...
在《Spring 2.5 开发手册》中,会详细解释这些特性的使用方法、配置细节以及最佳实践,帮助开发者充分利用Spring框架,构建稳定、高效的应用系统。通过阅读这份手册,开发者可以深入理解Spring 2.5的工作原理,提升...
Spring 2.5增强了测试支持,提供了TestContext框架,支持集成测试,包括对Spring配置的加载、事务管理、模拟对象等。 **国际化** Spring提供了对国际化的支持,通过MessageSource接口可以方便地实现多语言环境下的...
9. **集成测试**:Spring 2.5提供了方便的测试支持,如TestContext框架,使得集成测试变得简单。它允许在测试环境中轻松地加载ApplicationContext,并支持模拟bean和交易管理。 10. **Portlet MVC**:除了传统的Web...
10. **测试支持**:Spring 2.5提供了全面的测试工具和API,如`Spring TestContext Framework`,使得单元测试和集成测试变得更加容易。 了解并熟练运用上述知识点,对于初学者来说,是快速掌握Spring框架的关键。...
8. **单元测试与集成测试支持**:Spring提供TestContext框架,支持Junit、Mockito等工具进行单元测试和集成测试,方便开发者编写和运行测试用例。 9. **Spring Expression Language (SpEL)**:SpEL是Spring提供的...
在测试方面,手册提供了关于单元测试和集成测试的指导,包括使用JUnit、Mockito等工具,以及Spring的TestContext框架,帮助开发者编写高质量的测试代码,确保软件的稳定性和可靠性。 最后,手册还包含了一些最佳...
8. **单元测试与集成测试**:Spring提供了强大的测试支持,包括Mock对象、TestContext框架等,使得开发者能够编写高效的单元测试和集成测试。 9. **Spring Web Services**:2.5版本还支持创建和消费Web服务,包括...
9. **测试支持**:Spring提供了一系列测试工具和API,方便进行单元测试和集成测试,包括Mock对象和TestContext框架。 10. **Spring Batch**:对于批量处理任务,Spring Batch提供了一套完整的解决方案,包括读取、...
8. **测试支持**:Spring提供了一套完整的测试框架,包括Mock对象、TestContext框架等,方便进行单元测试和集成测试。 9. **国际化(Internationalization,i18n)**:Spring通过MessageSource接口支持多语言环境,...
9. **集成测试**:Spring 2.5提供了更全面的测试支持,包括Mock对象、TestContext框架和`@RunWith(SpringJUnit4ClassRunner.class)`注解,便于进行单元测试和集成测试。 10. **国际化支持**:Spring 2.5加强了对...