保持领先
VMware 提供培训和认证,助您快速进步。
了解更多正如 Juergen Hoeller 在宣布 Spring Framework 3.2 RC1 发布 的文章中提到的,Spring 团队在测试支持方面引入了一些令人兴奋的新特性。最重要的是,我们为测试 Web 应用添加了头等支持。[1]
请注意:本文转载自我的 Swiftmind 公司博客的 交叉发布文章。
在本文中,我们将首先介绍 Spring Framework 中的一些通用新增测试特性,然后详细讨论使用 WebApplicationContext
以及 request 和 session 作用域 Bean 的测试支持。最后,我们将介绍对 ApplicationContextInitializer
的支持,并简要讨论应用程序上下文层次结构测试的路线图。
Rossen Stoyanchev 稍后将就全新的 Spring MVC Test 框架发表一篇详细文章,该框架为测试 Spring MVC 应用提供了头等支持。因此,请务必继续关注,因为它建立在本文稍后讨论的基本 Web 测试支持之上。
spring-test
模块现在基于并支持 JUnit 4.10 和 TestNG 6.5.2,并且 spring-test
现在依赖于 Maven artifact junit:junit-dep
而不是 junit:junit
,这意味着您可以完全控制对 Hamcrest 库(例如,hamcrest-core
、hamcrest-all
等)的依赖项。
泛型工厂方法 是使用 Java 泛型实现 工厂方法设计模式 的方法。以下是一些泛型工厂方法的示例签名:
public static <T> T mock(Class<T> clazz) { ... }
public static <T> T proxy(T obj) { ... }
在 Spring 配置中使用 泛型工厂方法 并非仅限于测试,但像 EasyMock.createMock(MyService.class)
或 Mockito.mock(MyService.class)
这样的泛型工厂方法通常用于在测试应用程序上下文中为 Spring Bean 创建动态 Mock。例如,在 Spring Framework 3.2 之前,以下配置可能无法将 OrderRepository
自动装配到 OrderService
中。原因是,根据 Bean 在应用程序上下文中初始化的顺序,Spring 可能会将 orderRepository
Bean 的类型推断为 java.lang.Object
而不是 com.example.repository.OrderRepository
。
<beans>
<!-- OrderService is autowired with OrderRepository -->
<context:component-scan base-package="com.example.service"/>
<bean id="orderRepository" class="org.easymock.EasyMock"
factory-method="createMock"
c:_="com.example.repository.OrderRepository" />
</beans>
在 Spring 3.2 中,工厂方法的泛型返回类型现在可以正确推断,并且对 Mock 的 按类型自动装配 应该可以按预期工作。因此,像 MockitoFactoryBean
、EasyMockFactoryBean
或 Springockito 这样的自定义变通方法可能不再需要。
我们引入了 MockEnvironment
,它补充了现有的 MockPropertySource
,以完成对 Spring 3.1 中引入的环境和属性源抽象进行 Mock 的支持。
关于 Web 组件的单元测试支持,我们为现有的 Servlet API Mock(例如 MockServletContext
、MockHttpSession
、MockFilterChain
和 MockRequestDispatcher
)添加了新特性,并且引入了与 REST Web 服务相关的新 Mock:用于客户端的 MockClientHttpRequest
和 MockClientHttpResponse
,以及用于服务器端的 MockHttpInputMessage
和 MockHttpOutputMessage
。
在 Spring 3.2 中,我们弃用了 SimpleJdbcTestUtils
,转而推荐改进后的 JdbcTestUtils
类,该类除了提供 SimpleJdbcTestUtils
之前提供的所有功能外,还提供了新的 countRowsInTableWhere()
和 dropTables()
工具方法。这些更改有助于避免与使用已弃用的 SimpleJdbcTemplate
相关的编译器警告,并提供了一种便捷的方式,可以使用 WHERE
子句计算表中的行数以及删除表列表。同样,AbstractTransactionalJUnit4SpringContextTests
和 AbstractTransactionalTestNGSpringContextTests
已被改造,增加了 jdbcTemplate
实例变量以及委托给 JdbcTestUtils
中对应方法的 countRowsInTableWhere()
和 dropTables()
方法。
如果您熟悉 Spring TestContext Framework 中对事务性集成测试的支持,那么您可能知道用于测试的事务管理器按照约定必须命名为 "transactionManager"。自 Spring 2.5 起,可以通过 @TransactionConfiguration
注解(例如,@TransactionConfiguration(transactionManager="txMgr")
)覆盖此约定;但是,如果应用程序上下文中存在单个 PlatformTransactionManger
,则不再需要使用此注解。换句话说,只要上下文中只定义了一个事务管理器,就不再需要 指定 该事务管理器的名称:如果只有一个,TestContext 框架将直接使用它。
Spring 3.1 引入了 TransactionManagementConfigurer
接口,用于在使用 @Configuration
类结合 @EnableTransactionManagement
时(即,与使用带有 <tx:annotation-driven />
的 XML 配置相对)编程指定用于 @Transactional
方法的事务管理器。因此,自 Spring 3.2 起,如果您的某个组件(例如,通常是 @Configuration
类)实现了 TransactionManagementConfigurer
,TestContext 框架将使用该组件指定的事务管理器。
本文的其余部分明确讨论 Spring TestContext Framework 中的新特性。如果您已经熟悉 TestContext 框架,请随意跳到下一节。否则,您可能希望首先通过以下段落中的链接熟悉相关信息。
在 Spring 2.5 中,我们引入了 Spring TestContext Framework,它提供了注解驱动的集成测试支持,可与 JUnit 或 TestNG 一起使用。本文中的示例将侧重于基于 JUnit 的测试,但这里使用的所有特性也适用于 TestNG。
在 Spring 3.1 中,我们修订了 Spring TestContext Framework,并添加了对使用 @Configuration
类和环境 Profile 进行测试的支持。
WebApplicationContext
?@WebAppConfiguration
注解您的测试类。这真的很简单。在您的测试类上存在 @WebAppConfiguration
注解会指示 TestContext 框架 (TCF) 为您的集成测试加载一个 WebApplicationContext
(WAC)。在后台,TCF 会确保创建一个 MockServletContext
并提供给您的测试 WAC。默认情况下,您的 MockServletContext
的基础资源路径将设置为 "src/main/webapp"。这被解释为相对于 JVM 根目录(即,通常是您的项目路径)的路径。如果您熟悉 Maven 项目中 Web 应用程序的目录结构,您会知道 "src/main/webapp" 是 WAR 根目录的默认位置。如果您需要覆盖此默认设置,只需为 @WebAppConfiguration
注解提供一个备用路径(例如,@WebAppConfiguration("src/test/webapp")
)。如果您希望引用类路径而不是文件系统中的基础资源路径,只需使用 Spring 的 classpath: 前缀即可。
请注意,Spring 对 WebApplicationContexts
的测试支持与对标准 ApplicationContexts
的支持同等重要。使用 WebApplicationContext
进行测试时,您可以通过 @ContextConfiguration
自由地声明 XML 配置文件或 @Configuration
类。当然,您也可以自由使用任何其他测试注解,例如 @TestExecutionListeners
、@TransactionConfiguration
、@ActiveProfiles
等。
让我们看一些示例...
约定
@RunWith(SpringJUnit4ClassRunner.class)
// defaults to "file:src/main/webapp"
@WebAppConfiguration
// detects "WacTests-context.xml" in same package
// or static nested @Configuration class
@ContextConfiguration
public class WacTests {
//...
}
上面的示例展示了 TestContext 框架对 约定优于配置 的支持。如果您使用 @WebAppConfiguration
注解测试类而未指定资源基础路径,资源路径将默认设置为 "file:src/main/webapp"。同样,如果您声明 @ContextConfiguration
而未指定资源 locations
、注解的 classes
或上下文 initializers
,Spring 将尝试使用约定检测配置的存在(即,在与 WacTests
类相同的包中的 "WacTests-context.xml" 或静态嵌套的 @Configuration
类)。
默认资源语义
@RunWith(SpringJUnit4ClassRunner.class)
// file system resource
@WebAppConfiguration("webapp")
// classpath resource
@ContextConfiguration("/spring/test-servlet-config.xml")
public class WacTests {
//...
}
此示例展示了如何使用 @WebAppConfiguration
显式声明资源基础路径,以及使用 @ContextConfiguration
显式声明 XML 资源位置。这里需要注意的重要一点是,这两种注解的路径语义不同。默认情况下,@WebAppConfiguration
的资源路径是基于文件系统的;而 @ContextConfiguration
的资源位置是基于类路径的。
显式资源语义
@RunWith(SpringJUnit4ClassRunner.class)
// classpath resource
@WebAppConfiguration("classpath:test-web-resources")
// file system resource
@ContextConfiguration(
"file:src/main/webapp/WEB-INF/servlet-config.xml")
public class WacTests {
//...
}
在第三个示例中,我们看到可以通过指定 Spring 资源前缀来覆盖这两种注解的默认资源语义。将此示例中的注释与上一个示例进行对比。
为了提供全面的 Web 测试支持,Spring 3.2 引入了一个新的 ServletTestExecutionListener
,该监听器默认启用。当针对 WebApplicationContext
进行测试时,此 TestExecutionListener 会在每个测试方法之前通过 Spring Web 的 RequestContextHolder
设置默认的线程本地状态,并根据通过 @WebAppConfiguration
配置的基础资源路径创建一个 MockHttpServletRequest
、MockHttpServletResponse
和 ServletWebRequest
。ServletTestExecutionListener
还确保可以将 MockHttpServletResponse
和 ServletWebRequest
注入到测试实例中,并在测试完成后清理线程本地状态。
一旦您的测试加载了 WebApplicationContext
,您可能会发现需要与 Web Mock 进行交互——例如,设置您的测试夹具或在调用 Web 组件后执行断言。以下示例演示了哪些 Mock 可以自动装配到您的测试实例中。请注意,WebApplicationContext
和 MockServletContext
在整个测试套件中都会被缓存;而其他 Mock 则由 ServletTestExecutionListener
按每个测试方法进行管理。
注入 Mock
@WebAppConfiguration
@ContextConfiguration
public class WacTests {
@Autowired WebApplicationContext wac; // cached
@Autowired MockServletContext servletContext; // cached
@Autowired MockHttpSession session;
@Autowired MockHttpServletRequest request;
@Autowired MockHttpServletResponse response;
@Autowired ServletWebRequest webRequest;
//...
}
请求和会话作用域 Bean Spring 已经支持好几年了,但测试它们总是有点复杂。自 Spring 3.2 起,按照以下步骤测试您的请求作用域和会话作用域 Bean 变得非常简单:
@WebAppConfiguration
注解您的测试类,确保为您的测试加载了 WebApplicationContext
。WebApplicationContext
中获取的 Web 组件(即,通过依赖注入)。以下代码片段显示了一个登录用例的 XML 配置。请注意,userService
Bean 依赖于一个请求作用域的 loginAction
Bean。此外,LoginAction
是使用 SpEL 表达式 实例化的,该表达式从当前的 HTTP 请求中检索用户名和密码。在我们的测试中,我们将希望通过 TestContext 框架管理的 Mock 来配置这些请求参数。
请求作用域 Bean 配置
<beans>
<bean id="userService"
class="com.example.SimpleUserService"
c:loginAction-ref="loginAction" />
<bean id="loginAction" class="com.example.LoginAction"
c:username="#{request.getParameter('user')}"
c:password="#{request.getParameter('pswd')}"
scope="request">
<aop:scoped-proxy />
</bean>
</beans>
在 RequestScopedBeanTests
中,我们将 UserService
(即,被测试的对象)和 MockHttpServletRequest
都注入到我们的测试实例中。在我们的 requestScope()
测试方法中,通过在提供的 MockHttpServletRequest
中设置请求参数来设置我们的测试夹具。当在我们的 userService
上调用 loginUser()
方法时,我们可以确定用户服务可以访问当前 MockHttpServletRequest
(即,我们刚刚设置了参数的那个请求)对应的请求作用域的 loginAction
。然后,我们可以根据已知的用户名和密码输入对结果执行断言。
请求作用域 Bean 测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class RequestScopedBeanTests {
@Autowired UserService userService;
@Autowired MockHttpServletRequest request;
@Test
public void requestScope() {
request.setParameter("user", "enigma");
request.setParameter("pswd", "$pr!ng");
LoginResults results = userService.loginUser();
// assert results
}
}
以下代码片段类似于我们上面看到的请求作用域 Bean 的示例;然而,这次 userService
Bean 依赖于一个会话作用域的 userPreferences
Bean。请注意,UserPreferences
Bean 是使用一个 SpEL 表达式实例化的,该表达式从当前的 HTTP 会话中检索 theme。在我们的测试中,我们将需要在 TestContext 框架管理的 Mock 会话中配置一个主题。
会话作用域 Bean 配置
<beans>
<bean id="userService"
class="com.example.SimpleUserService"
c:userPreferences-ref="userPreferences" />
<bean id="userPreferences"
class="com.example.UserPreferences"
c:theme="#{session.getAttribute('theme')}"
scope="session">
<aop:scoped-proxy />
</bean>
</beans>
在 SessionScopedBeanTests
中,我们将 UserService
和 MockHttpSession
注入到我们的测试实例中。在我们的 sessionScope()
测试方法中,通过在提供的 MockHttpSession
中设置预期的 "theme" 属性来设置我们的测试夹具。当在我们的 userService
上调用 processUserPreferences()
方法时,我们可以确定用户服务可以访问当前 MockHttpSession
对应的会话作用域的 userPreferences
,并且我们可以根据配置的主题对结果执行断言。
会话作用域 Bean 测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class SessionScopedBeanTests {
@Autowired UserService userService;
@Autowired MockHttpSession session;
@Test
public void sessionScope() throws Exception {
session.setAttribute("theme", "blue");
Results results = userService.processUserPreferences();
// assert results
}
}
Spring 3.1 引入了 ApplicationContextInitializer
接口,该接口允许以编程方式初始化 ConfigurableApplicationContext
——例如,针对 Spring Environment
抽象注册属性源或激活 Bean 定义 Profile。可以通过在 web.xml
中为 ContextLoaderListener
指定 contextInitializerClasses
的 context-param
和为 DispatcherServlet
指定 init-param
来配置 Initializer。
要在集成测试中使用上下文 Initializer,只需通过 Spring 3.2 中引入的新 initializers
属性在 @ContextConfiguration
中声明 Initializer 类即可。可以通过 inheritInitializers
属性(默认为 true
)控制 Initializer 在测试类层次结构中的继承。由于 ApplicationContextInitializer
提供了初始化应用程序上下文的完全编程方法,因此 Initializer 可以选择性地配置整个上下文。换句话话,如果在通过 @ContextConfiguration
配置的集成测试中声明了 Initializer,则不再绝对需要 XML 资源位置或注解的类。最后同样重要的是,上下文 Initializer 是根据 Spring 的 Ordered
接口或 @Order
注解 排序 的。
以下代码示例展示了在集成测试中使用上下文 Initializer 的各种方式。第一个示例展示了如何结合 XML 资源位置配置单个 Initializer。下一个示例声明了多个上下文 Initializer。第三个示例展示了在类层次结构中使用 Initializer 的情况,其中在 ExtendedTest
中声明的上下文 Initializer 列表将与在 BaseTest
中声明的合并。请注意,Initializer 的调用顺序受 Spring 的 Ordered
接口实现或 @Order
注解的存在影响。第四个示例与第三个示例相同,只是 @ContextConfiguration
中的 inheritInitializers
属性设置为 false
。结果是,父类中声明的任何上下文 Initializer 都将被忽略(即,被覆盖)。最后一个示例展示了 ApplicationContext
可以仅从上下文 Initializer 加载,而无需声明 XML 资源位置或注解的类。
单个 Initializer
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
locations = "/app-config.xml",
initializers = CustomInitializer.class)
public class ApplicationContextInitializerTests {}
多个 Initializer
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
locations = "/app-config.xml",
initializers = {
PropertySourceInitializer.class,
ProfileInitializer.class
})
public class ApplicationContextInitializerTests {}
合并的 Initializer
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
classes = BaseConfig.class,
initializers = BaseInitializer.class)
public class BaseTest {}
@ContextConfiguration(
classes = ExtendedConfig.class,
initializers = ExtendedInitializer.class)
public class ExtendedTest extends BaseTest {}
被覆盖的 Initializer
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
classes = BaseConfig.class,
initializers = BaseInitializer.class)
public class BaseTest {}
@ContextConfiguration(
classes = ExtendedConfig.class,
initializers = ExtendedInitializer.class,
inheritInitializers = false)
public class ExtendedTest extends BaseTest {}
不含资源的 Initializer
// does not declare 'locations' or 'classes'
@ContextConfiguration(
initializers = EntireAppInitializer.class)
public class InitializerWithoutConfigFilesOrClassesTest {}
一旦 TestContext 框架为一个测试加载了 ApplicationContext
,该上下文将被缓存并用于在同一测试套件中声明相同唯一上下文配置的所有后续测试。这里需要记住的重要一点是,ApplicationContext
通过其 上下文缓存键 唯一标识(即,用于加载它的配置参数组合)。
自 Spring 3.2 起,ApplicationContextInitializer
类也被包含在 上下文缓存键 中。此外,如果上下文是 WebApplicationContext
,其基础资源路径(通过 @WebAppConfiguration
定义)也将包含在 上下文缓存键 中。有关缓存的更多详细信息,请查阅参考手册的上下文缓存部分。
注意:截至 Spring Framework 3.2 RC1,尚未实现对上下文层次结构的支持。
在由 Spring TestContext Framework 管理的集成测试中,目前仅支持扁平的、非层次结构的上下文。换句话说,目前没有简单的方法为测试创建具有父子关系的上下文。但是生产部署中支持上下文层次结构。因此,能够测试它们将是一件好事。
考虑到这一点,Spring 团队希望引入集成测试支持,用于加载具有父上下文的测试应用程序上下文,理想情况下应支持以下常见层次结构:
WebApplicationContext
← Dispatcher WebApplicationContext
WebApplicationContext
← Dispatcher WebApplicationContext
目前的提案包括引入一个新的 @ContextHierarchy
注解,它将包含嵌套的 @ContextConfiguration
声明,以及在 @ContextConfiguration
中添加一个新的 name
属性,该属性可用于在上下文层次结构中 合并 或 覆盖 命名配置。
为了更好地说明此提案,让我们看几个示例...
AppCtxHierarchyTests
演示了在单个测试类中声明的父子上下文层次结构,其中上下文是标准上下文(即,非 Web)。
具有上下文层次结构的单个测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextHierarchy({
@ContextConfiguration("parent.xml"),
@ContextConfiguration("child.xml")
})
public class AppCtxHierarchyTests {}
ControllerIntegrationTests
演示了在单个测试类中声明的父子上下文层次结构,其中上下文是 WebApplicationContexts
并模拟了典型的 Spring MVC 部署。
Root WAC & Dispatcher WAC
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextHierarchy({
@ContextConfiguration(
name = "root",
classes = WebAppConfig.class),
@ContextConfiguration(
name = "dispatcher",
locations = "/spring/dispatcher-config.xml")
})
public class ControllerIntegrationTests {}
以下代码列表展示了如何在测试类层次结构中构建上下文层次结构,其中测试类层次结构中的每一层负责配置其在上下文层次结构中的对应层。在这两个子类中执行测试将导致加载(和缓存)三个应用程序上下文和两个不同的上下文层次结构。
类与上下文层次结构
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(
"file:src/main/webapp/WEB-INF/applicationContext.xml")
public abstract class AbstractWebTests{}
@ContextHierarchy(@ContextConfiguration("/spring/soap-ws-config.xml"))
public class SoapWebServiceTests extends AbstractWebTests{}
@ContextHierarchy(@ContextConfiguration("/spring/rest-ws-config.xml"))
public class RestWebServiceTests extends AbstractWebTests{}
如果您对上下文层次结构的提案感兴趣或想参与讨论,请随意 关注 以下 JIRA 问题并向我们提供您的反馈。
Spring Framework 3.2 引入了多项全新的测试特性,重点关注对测试 Web 应用的头等支持。我们鼓励您尽快尝试这些特性并向我们提供反馈。此外,请继续关注 Rossen Stoyanchev 关于全新 Spring MVC Test 框架的后续文章。如果您发现任何 Bug 或有任何改进建议,现在是时候采取行动了!
[1] 参考手册尚未更新以反映对 Web 应用程序的测试支持,但这些特性在 Spring 3.2 GA 发布时肯定会得到充分文档化。