跳到主要内容
版本:7.0.3

上下文层次结构

Hunyuan 7b 中英对照 Context Hierarchies

在编写依赖于已加载的Spring ApplicationContext的集成测试时,通常只需针对单个ApplicationContext进行测试即可。然而,在某些情况下,针对ApplicationContext实例的层级结构进行测试会更有益,甚至是必要的。例如,如果你正在开发一个Spring MVC Web应用程序,通常会有一个由Spring的ContextLoaderListener加载的根WebApplicationContext,以及一个由Spring的DispatcherServlet加载的子WebApplicationContext。这样就形成了一个父子上下文层次结构:共享组件和基础设施配置在根上下文中声明,而在子上下文中则被特定于Web的组件所使用。另一个应用场景可以在Spring Batch应用程序中找到,在那里通常会有一个父上下文用于提供共享批处理基础设施的配置,而子上下文则用于特定批处理作业的配置。

你可以通过使用@ContextHierarchy注解来声明上下文配置,从而编写利用上下文层次结构的集成测试,该注解可以应用于单个测试类,也可以应用于测试类层次结构中。如果在测试类层次结构中的多个类上声明了上下文层次结构,那么你还可以合并或覆盖该上下文层次结构中特定命名层次的上下文配置。在合并层次结构中给定层次的配置时,配置资源类型(即XML配置文件或组件类)必须保持一致。否则,即使上下文层次结构中的不同层次使用不同的资源类型进行配置也是完全可以接受的。

备注

如果在测试中使用了 @DirtiesContext,而该测试的上下文是作为上下文层次结构的一部分进行配置的,那么你可以使用 hierarchyMode 标志来控制上下文缓存的清除方式。

有关更多详细信息,请参阅 Spring 测试注解 中对 @DirtiesContext 的讨论,以及 @DirtiesContext 的 javadoc 文档。

本节中基于JUnit Jupiter的示例展示了需要使用上下文层次结构的集成测试的常见配置场景。

具有上下文层次结构的单一测试类

ControllerIntegrationTests 代表了一个典型的 Spring MVC Web 应用程序集成测试场景,它定义了一个由两个层级组成的上下文层次结构:一个是根级别的 WebApplicationContext(通过使用 TestAppConfig @Configuration 类加载),另一个是分发器 Servlet 的 WebApplicationContext(通过使用 WebConfig @Configuration 类加载)。自动注入到测试实例中的 WebApplicationContext 是子上下文的那个(即层次结构中最底层的上下文)。以下列表展示了这种配置场景:

@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextHierarchy({
@ContextConfiguration(classes = TestAppConfig.class),
@ContextConfiguration(classes = WebConfig.class)
})
class ControllerIntegrationTests {

@Autowired
WebApplicationContext wac;

// ...
}

具有隐式父上下文的类层次结构

在这个示例中,测试类在测试类层次结构内定义了一个上下文层次结构。AbstractWebTests声明了由Spring驱动的Web应用程序中的根WebApplicationContext的配置。然而,请注意AbstractWebTests并没有声明@ContextHierarchy注解。因此,AbstractWebTests的子类可以选择性地参与这个上下文层次结构,或者遵循@ContextConfiguration的标准语义。SoapWebServiceTestsRestWebServiceTests都继承自AbstractWebTests,并通过使用@ContextHierarchy来定义自己的上下文层次结构。结果是,会加载三个应用程序上下文(每个@ContextConfiguration的声明对应一个上下文),而基于AbstractWebTests中配置加载的应用程序上下文被设置为各个具体子类所加载上下文的父上下文。以下列表展示了这种配置场景:

@ExtendWith(SpringExtension.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 {}

具有合并上下文层次结构的类层次结构配置

在这个例子中,展示了如何使用命名的层次结构级别来合并特定层级的配置。BaseTests 定义了层次结构中的两个级别:parentchildExtendedTests 继承自 BaseTests,并指示 Spring TestContext 框架合并 child 层级结构的配置,通过确保在 @ContextConfiguration 中的 name 属性中声明的名称都是 child 来实现这一点。结果是加载了三个应用程序上下文:一个用于 /app-config.xml,一个用于 /user-config.xml,还有一个用于 {"/user-config.xml", "/order-config.xml"}。与之前的例子一样,从 /app-config.xml 加载的应用程序上下文被设置为从 /user-config.xml{"/user-config.xml", "/order-config.xml"} 加载的上下文的父上下文。以下列表展示了这种配置场景:

@ExtendWith(SpringExtension.class)
@ContextHierarchy({
@ContextConfiguration(name = "parent", locations = "/app-config.xml"),
@ContextConfiguration(name = "child", locations = "/user-config.xml")
})
class BaseTests {}

@ContextHierarchy(
@ContextConfiguration(name = "child", locations = "/order-config.xml")
)
class ExtendedTests extends BaseTests {}

具有重写上下文层次结构的类层次结构配置

与之前的例子不同,这个例子展示了如何通过将@ContextConfiguration中的inheritLocations标志设置为false来覆盖上下文层次结构中某个特定命名级别的配置。因此,ExtendedTests的应用上下文仅从 /test-user-config.xml 中加载,其父上下文则设置为从 /app-config.xml 加载的上下文。以下列表显示了这种配置场景:

@ExtendWith(SpringExtension.class)
@ContextHierarchy({
@ContextConfiguration(name = "parent", locations = "/app-config.xml"),
@ContextConfiguration(name = "child", locations = "/user-config.xml")
})
class BaseTests {}

@ContextHierarchy(
@ContextConfiguration(
name = "child",
locations = "/test-user-config.xml",
inheritLocations = false
))
class ExtendedTests extends BaseTests {}

带有bean覆盖的上下文层次结构

@ContextHierarchybean覆盖(如@TestBean@MockitoBean@MockitoSpyBean)结合使用时,可能需要或希望将覆盖应用到上下文层次结构中的单个层级。为此,bean覆盖必须指定一个与通过@ContextConfiguration中的name属性配置的名称相匹配的上下文名称。

以下测试类将第二层层次的名称配置为“user-config”,并且同时指定在名为“user-config”的上下文中,UserService应该被包装在一个Mockito间谍(spy)中。因此,Spring将只尝试在“user-config”上下文中创建这个间谍,而不会尝试在父上下文中创建它。

@ExtendWith(SpringExtension.class)
@ContextHierarchy({
@ContextConfiguration(classes = AppConfig.class),
@ContextConfiguration(classes = UserConfig.class, name = "user-config")
})
class IntegrationTests {

@MockitoSpyBean(contextName = "user-config")
UserService userService;

// ...
}

在上下文层次结构的不同级别应用bean覆盖时,你可能需要将所有被覆盖的bean实例注入到测试类中,以便与它们进行交互——例如,为了配置mock的stubbing。然而,@Autowired总是会注入在上下文层次结构最低级别找到的匹配bean。因此,要从上下文层次结构的特定级别注入被覆盖的bean实例,你需要用适当的bean覆盖注解来标注字段,并配置该上下文级别的名称。

以下测试类将层次级别的名称配置为“parent”和“child”。它还声明了两个PropertyService字段,这些字段被配置为在相应的上下文中使用Mockito的mock来创建或替换PropertyService bean,分别命名为“parent”和“child”。因此,“parent”上下文中的mock将被注入到propertyServiceInParent字段中,“child”上下文中的mock将被注入到propertyServiceInChild字段中。

@ExtendWith(SpringExtension.class)
@ContextHierarchy({
@ContextConfiguration(classes = ParentConfig.class, name = "parent"),
@ContextConfiguration(classes = ChildConfig.class, name = "child")
})
class IntegrationTests {

@MockitoBean(contextName = "parent")
PropertyService propertyServiceInParent;

@MockitoBean(contextName = "child")
PropertyService propertyServiceInChild;

// ...
}