Spring Framework 3.2 RC1: 新的测试功能

工程 | Sam Brannen | 2012年11月07日 | ...

正如Juergen Hoeller在其宣布Spring Framework 3.2 RC1发布的博文中提到的那样,Spring团队在测试支持方面引入了一些令人兴奋的新功能。最重要的是,我们为测试Web应用程序添加了一流的支持。[1]

      请注意:这是一篇来自我的Swiftmind公司博客的交叉发布

在这篇文章中,我们将首先介绍Spring Framework中一些通用的新测试功能,然后详细讨论对使用WebApplicationContext以及请求(request)会话(session)作用域bean进行测试的支持。最后,我们将介绍对ApplicationContextInitializers的支持,并简要讨论使用应用程序上下文层次结构进行测试的路线图。

Rossen Stoyanchev稍后将发表一篇详细介绍新的Spring MVC Test框架的博文,该框架为测试Spring MVC应用程序提供了一流的支持。因此,请务必关注,因为它基于本文后面讨论的基本Web测试支持。



通用新功能和更新


构建和依赖

spring-test模块现在基于并支持JUnit 4.10和TestNG 6.5.2构建,并且spring-test现在依赖于junit:junit-dep Maven工件而不是junit:junit,这意味着您可以完全控制对Hamcrest库(例如,hamcrest-corehamcrest-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创建动态模拟。例如,在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中,现在可以正确推断工厂方法的泛型返回类型,并且模拟的按类型自动装配应该按预期工作。因此,自定义的变通方法,例如MockitoFactoryBeanEasyMockFactoryBeanSpringockito,可能不再需要。

模拟对象

我们引入了MockEnvironment,它补充了现有的MockPropertySource,以完成对Spring 3.1中引入的环境和属性源抽象的模拟支持。

关于Web组件的单元测试支持,我们为现有的Servlet API模拟(例如MockServletContextMockHttpSessionMockFilterChainMockRequestDispatcher)添加了新功能,并且我们引入了与REST Web服务相关的新模拟:客户端的MockClientHttpRequestMockClientHttpResponse以及服务器端的MockHttpInputMessageMockHttpOutputMessage

JDBC测试支持

在Spring 3.2中,我们已弃用SimpleJdbcTestUtils,取而代之的是改进的JdbcTestUtils类,该类除了SimpleJdbcTestUtils之前提供的所有功能外,还提供了新的countRowsInTableWhere()dropTables()实用方法。这些更改有助于避免与使用已弃用的SimpleJdbcTemplate相关的编译器警告,并提供了一种方便的方法,可以使用WHERE子句计算表中行数以及删除表列表。类似地,AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests也已改造,添加了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 框架


本文的其余部分明确讨论Spring TestContext框架中的新功能。如果您已经熟悉TestContext框架,请随意跳到下一节。否则,您可能需要首先熟悉以下段落中通过链接提供的信息。

在Spring 2.5中,我们引入了Spring TestContext Framework,它提供了注释驱动的集成测试支持,可与JUnit或TestNG一起使用。本文中的示例将重点关注基于JUnit的测试,但此处使用的所有功能也适用于TestNG。

在Spring 3.1中,我们修订了Spring TestContext Framework,增加了对使用@Configuration类和环境配置文件进行测试的支持。



加载 WebApplicationContext


  • 问题:如何告知TestContext框架加载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模拟


为了提供全面的Web测试支持,Spring 3.2引入了一个新的ServletTestExecutionListener,默认情况下启用。在对WebApplicationContext进行测试时,此TestExecutionListener会在每个测试方法之前通过Spring Web的RequestContextHolder设置默认的线程局部状态,并根据通过@WebAppConfiguration配置的基本资源路径创建MockHttpServletRequestMockHttpServletResponseServletWebRequestServletTestExecutionListener还确保MockHttpServletResponseServletWebRequest可以注入到测试实例中,并且在测试完成后它会清理线程局部状态。

一旦为您的测试加载了WebApplicationContext,您可能会发现需要与Web模拟进行交互——例如,设置您的测试夹具或在调用Web组件后执行断言。以下示例演示了哪些模拟可以自动装配到您的测试实例中。请注意,WebApplicationContextMockServletContext都在整个测试套件中缓存;而其他模拟由ServletTestExecutionListener按测试方法进行管理。

注入模拟


@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


请求和会话作用域的bean在Spring中已经支持了好几年,但测试它们一直有些复杂。从Spring 3.2开始,通过遵循以下步骤测试您的请求作用域和会话作用域的bean变得轻而易举

  1. 通过使用@WebAppConfiguration注解您的测试类,确保为您的测试加载了一个WebApplicationContext
  2. 将模拟请求或会话注入到您的测试实例中,并根据需要准备您的测试夹具。
  3. 调用您从配置的WebApplicationContext(即,通过依赖注入)检索到的Web组件。
  4. 对模拟执行断言。

以下代码片段显示了登录用例的XML配置。请注意,userService bean依赖于一个请求作用域的loginAction bean。此外,LoginAction使用SpEL表达式实例化,这些表达式从当前的HTTP请求中检索用户名和密码。在我们的测试中,我们将希望通过TestContext框架管理的模拟来配置这些请求参数。

请求作用域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框架管理的模拟会话中配置一个主题。

会话作用域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中,我们将UserServiceMockHttpSession注入到我们的测试实例中。在sessionScope()测试方法中,我们通过在提供的MockHttpSession中设置预期的“主题”属性来设置测试夹具。当在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定义配置文件。初始化器可以在web.xml中配置,通过ContextLoaderListenercontext-paramDispatcherServletinit-param来指定contextInitializerClasses

要在集成测试中使用上下文初始化器,只需通过Spring 3.2中引入的新的initializers属性在@ContextConfiguration中声明初始化器类。通过inheritInitializers属性可以控制跨测试类层次结构的初始化器继承,该属性默认为true。由于ApplicationContextInitializer提供了一种完全编程化的方法来初始化应用程序上下文,因此初始化器可以选择性地配置整个上下文。换句话说,如果声明了初始化器,则通过@ContextConfiguration配置的集成测试不再绝对需要XML资源位置或注解类。最后但同样重要的是,上下文初始化器根据Spring的Ordered接口或@Order注解进行排序

以下代码示例演示了在集成测试中使用上下文初始化器的各种方法。第一个示例展示了如何将单个初始化器与XML资源位置结合配置。下一个示例声明了多个上下文初始化器。第三个列表演示了在类层次结构中使用初始化器,其中ExtendedTest中声明的上下文初始化器列表将与BaseTest中声明的初始化器合并。请记住,初始化器的调用顺序受Spring的Ordered接口实现或@Order注解的存在影响。第四个示例与第三个示例相同,只是@ContextConfiguration中的inheritInitializers属性已设置为false。结果是,父类中声明的任何上下文初始化器都将被忽略(即被覆盖)。最后一个列表演示了ApplicationContext可以仅从上下文初始化器加载,而无需声明XML资源位置或注解类。

单个初始化器


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
    locations = "/app-config.xml",
    initializers = CustomInitializer.class)
public class ApplicationContextInitializerTests {}

多个初始化器


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
  locations = "/app-config.xml",
  initializers = {
    PropertySourceInitializer.class,
    ProfileInitializer.class
  })
public class ApplicationContextInitializerTests {}

合并初始化器


@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 {}

被覆盖的初始化器


@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 {}

不带资源的初始化器


// 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
  • EAR ← 根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部署。

根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问题并向我们提供您的反馈。

  • SPR-5613: 上下文层次结构支持
  • SPR-9863: Web上下文层次结构支持


总结


Spring Framework 3.2引入了多项新测试功能,重点是为测试Web应用程序提供一流的支持。我们鼓励您尽快尝试这些功能并向我们提供反馈。此外,请继续关注Rossen Stoyanchev关于新的Spring MVC Test框架的后续文章。如果您发现任何错误或有任何改进建议,现在是采取行动的时候了!



[1] 参考手册尚未更新以反映对Web应用程序的测试支持,但这些功能肯定会在Spring 3.2 GA中得到充分文档。



获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有