上下文缓存
一旦TestContext框架为某个测试加载了一个ApplicationContext(或WebApplicationContext),那么该上下文就会被缓存起来,并在同一个测试套件中后续的所有声明相同唯一上下文配置的测试中重复使用。要理解缓存的工作原理,重要的是要明白“唯一”和“测试套件”这两个概念的含义。
ApplicationContext可以通过用于加载它的配置参数组合来唯一标识。因此,这个唯一的配置参数组合被用来生成一个键,该键用于缓存上下文。TestContext框架使用以下配置参数来构建上下文缓存键:
-
locations(来自@ContextConfiguration) -
classes(来自@ContextConfiguration) -
contextInitializerClasses(来自@ContextConfiguration) -
contextCustomizers(来自ContextCustomizerFactory)——这包括@DynamicPropertySource方法、bean覆盖(如@TestBean、@MockitoBean、@MockitoSpyBean等),以及Spring Boot测试支持的各种功能。 -
contextLoader(来自@ContextConfiguration) -
parent(来自@ContextHierarchy) -
activeProfiles(来自@ActiveProfiles) -
propertySourceDescriptors(来自@TestPropertySource) -
propertySourceProperties(来自@TestPropertySource) -
resourceBasePath(来自@WebAppConfiguration)
例如,如果TestClassA在@ContextConfiguration的locations(或value)属性中指定了{"app-config.xml", "test-config.xml"},那么TestContext框架会加载相应的ApplicationContext,并将其存储在一个基于这些位置的static上下文缓存中。因此,如果TestClassB也为其locations指定了{"app-config.xml", "test-config.xml"}(无论是显式指定还是通过继承隐式指定),但没有定义@WebAppConfiguration,那么两个测试类将共享同一个ApplicationContext。这意味着加载应用程序上下文的开销只会在每个测试套件中发生一次,后续的测试执行会快得多。
测试套件和分叉进程
Spring TestContext框架将应用程序上下文存储在静态缓存中。这意味着上下文实际上是存储在一个static变量中的。换句话说,如果测试在不同的进程中运行,那么每次测试执行之间静态缓存会被清除,这实际上会使得缓存机制失效。
为了能够利用缓存机制,所有测试必须在同一进程中或同一测试套件内运行。这可以通过在IDE中将所有测试作为一个组来执行来实现。同样地,在使用Ant、Maven或Gradle等构建框架执行测试时,确保构建框架在测试之间不会分叉也很重要。例如,如果将Maven Surefire插件的forkMode设置为always或pertest,那么TestContext框架就无法在测试类之间缓存应用程序上下文,结果构建过程的运行速度会明显变慢。
上下文缓存的大小有一个默认的最大限制,为32。一旦达到最大大小,就会使用“最近最少使用”(LRU)淘汰策略来移除和关闭不再使用的上下文。你可以通过命令行或构建脚本来配置这个最大大小,方法是设置一个名为spring.test.context.cache.maxSize的JVM系统属性。另外,你也可以通过SpringProperties机制来设置相同的属性。
由于在给定的测试套件中加载大量应用上下文可能会导致套件运行时间过长,因此通常有必要准确了解已经加载和缓存的上下文数量。要查看底层上下文缓存的统计信息,可以将org.springframework.test.context.cache日志类别的日志级别设置为DEBUG。
在极少数情况下,如果某个测试操作破坏了应用程序上下文,导致需要重新加载(例如,通过修改bean定义或应用程序对象的状态),你可以在测试类或测试方法上添加@DirtiesContext注解(详见[Spring Testing Annotations]中的@DirtiesContext说明[../../annotations/integration-spring/annotation-dirtiescontext.md])。该注解会指示Spring在运行下一个需要相同应用程序上下文的测试之前,从缓存中清除当前上下文并重新构建它。需要注意的是,@DirtiesContext注解的支持是由DirtiesContextBeforeModesTestExecutionListener和DirtiesContextTestExecutionListener提供的,这两种监听器默认是启用的。
ApplicationContext 生命周期与控制台日志记录
当需要调试使用 Spring TestContext 框架执行的测试时,分析控制台输出(即输出到 SYSOUT 和 SYSERR 流中的内容)会非常有用。一些构建工具和 IDE 能够将控制台输出与特定的测试关联起来;然而,有些控制台输出却不易与特定测试关联。
对于由 Spring 框架本身或注册在 ApplicationContext 中的组件触发的控制台日志记录,了解 Spring TestContext 框架在测试套件中加载的 ApplicationContext 的生命周期是很重要的。
测试的 ApplicationContext 通常在准备测试类实例时被加载——例如,为了对测试实例的 @Autowired 字段进行依赖注入。这意味着在 ApplicationContext 初始化过程中触发的任何控制台日志记录通常无法与某个特定的测试方法关联起来。但是,如果根据 @DirtiesContext 的语义,在执行测试方法之前立即关闭上下文,那么在执行测试方法之前将会重新加载一个新的上下文实例。在这种情况下,IDE 或构建工具可能会将控制台日志记录与特定的测试方法关联起来。
测试的 ApplicationContext 可以通过以下几种情况之一被关闭:
- 根据
@DirtiesContext的语义关闭上下文。 - 由于遵循 LRU 弹出策略,上下文从缓存中自动被清除而关闭。
- 当测试套件的 JVM 终止时,通过 JVM 关闭钩子关闭上下文。
如果在某个特定测试方法之后根据 @DirtiesContext 的语义关闭了上下文,IDE 或构建工具可能会将控制台日志记录与该测试方法关联起来。如果在测试类之后根据 @DirtiesContext 的语义关闭了上下文,那么在 ApplicationContext 关闭过程中触发的任何控制台日志记录都无法与某个特定的测试方法关联起来。同样,通过 JVM 关闭钩子在关闭阶段触发的任何控制台日志记录也无法与某个特定的测试方法关联起来。
当 Spring ApplicationContext 通过 JVM 关闭钩子被关闭时,在关闭阶段执行的回调函数会在一个名为 SpringContextShutdownHook 的线程上执行。因此,如果你希望禁用在通过 JVM 关闭钩子关闭 ApplicationContext 时触发的控制台日志记录,你可以在日志框架中注册一个自定义过滤器,以便忽略该线程发起的任何日志记录。