Appearance
Spring Test
本文介绍Spring Test框架。Spring Test框架为单元测试和集成测试都提供了支持,本文主要关注集成测试。
关于什么是单元测试,什么是集成测试,以及两者的区别,参考附录1。
1. Spring Test原理
1.1 关键组件
Spring Test对集成测试提供的支持,主要在于Spring TestContext Framework。
Spring TestContext Framework由TestContextManager、TestContext、TestExecutionListener和ContextLoader四大模块组成。
TestContextManager:每个测试类都有一个对应的TestContextManager,用于管理测试执行过程,重要的两个组成部分就是TestContext和TestExecutionListener;TestContext:为测试提供上下文,所谓上下文,主要包含了两个方面:- 第一个方面:测试元数据(执行信息)。
TestContext记录了当前正在运行的“环境快照”:- 哪个类 (Test Class)
- 哪个方法 (Test Method)
- 执行状态:是正在执行
before钩子,还是正在执行真正的测试逻辑; - 异常信息:如果在执行过程中报错了,
TestContext也会记录下来;
- 第二个方面:Spring 容器(ApplicationContext)。
- 缓存机制:
TestContext负责管理容器。如果多个测试类使用的Spring容器是一模一样的,它会复用同一个容器实例,而不是每次都重启(这也是为什么 Spring 测试有时候第一次慢,后面快的原因)。 - 注入支持:它持有容器的引用,这样可以通过
TestContext获取到Bean,注入到当前的测试类字段中;
- 缓存机制:
- 第一个方面:测试元数据(执行信息)。
TestExecutionListener:在测试执行过程中,提供增强功能的接口,例如提供依赖注入功能、事务管理、SQL执行等功能,Spring Test框架提供了很多默认实现。ContextLoader:由TestContext持有,用于创建Spring容器,加载Bean。不同的实现可以用来从不同的地方加载Bean,例如从XML配置文件加载Bean、从注解加载Bean等。在实际中,实现SmartContextLoader用来自定义容器加载器。
因此,整个Spring TestContext Framework可以如图所示:

1.2 与测试引擎的联系
要运行测试用例,就需要有测试引擎,例如JUnit Jupiter,那Spring Test和JUnit Jupiter是如何关联上的呢?
通过JUnit的扩展机制
简单来说,Spring Test提供了一个JUnit扩展插件SpringExtension:
java
public class SpringExtension implements BeforeAllCallback, AfterAllCallback, TestInstancePostProcessor,
BeforeEachCallback, AfterEachCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback,
ParameterResolver {
// ...
}可以发现,SpringExtension实现了很多JUnit的钩子接口,也就是说在测试运行过程中,会调用SpringExtension中的特定方法,而SpringExtension又通过TestContextManager来与Spring Test框架产生联系。

例如,在SpringExtension的beforeAll()实现中:
java
@Override
public void beforeAll(ExtensionContext context) throws Exception {
// 1. 获取TestContextManager
TestContextManager testContextManager = getTestContextManager(context);
// 2. 注册方法调用,事务部分讲解
registerMethodInvoker(testContextManager, context);
// 3. 逐个调用TestExecutionListener的对应方法
testContextManager.beforeTestClass();
}getTestContextManager()如下:
java
static TestContextManager getTestContextManager(ExtensionContext context) {
Assert.notNull(context, "ExtensionContext must not be null");
Class<?> testClass = context.getRequiredTestClass();
Store store = getStore(context);
return store.getOrComputeIfAbsent(testClass, TestContextManager::new, TestContextManager.class);
}
private static Store getStore(ExtensionContext context) {
return context.getRoot().getStore(TEST_CONTEXT_MANAGER_NAMESPACE);
}实际就是使用了JUnit的Store机制,将TestContextManeger存在Store中,如果不在就新建一个。
org.springframework.test.context.TestContextManager#beforeTestClass源码(去掉相关日志打印代码)如下:
java
public void beforeTestClass() throws Exception {
try {
// 更新TestContext状态
getTestContext().updateState(null, null, null);
// 逐个调用TestExecutionListener的beforeTestClass方法
for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) {
try {
testExecutionListener.beforeTestClass(getTestContext());
}
catch (Throwable ex) {
ReflectionUtils.rethrowException(ex);
}
}
}
finally {
resetMethodInvoker();
}
}因此,SpringExtension就是JUnit与Spring TestContext Framework之间的桥梁。
2. Spring容器管理
说说Spring最重要的容器概念是如何引入测试的,Spring容器管理涉及创建容器(从哪里加载Bean)、Profile选择、环境配置(Environment)、容器缓存等内容。
2.1 创建容器
先来看看一个最基本的测试中的Spring容器案例:
java
@ExtendWith(SpringExtension.class)
public class Demo02 implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Test
public void test() {
System.out.println(applicationContext);
System.out.println("-------------------------------------");
Arrays.stream(applicationContext.getBeanDefinitionNames()).forEach(System.out::println);
}
}结果如下:
txt
org.springframework.context.support.GenericApplicationContext@22ffa91a, started on Sat Mar 14 20:30:46 CST 2026
-------------------------------------
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
org.springframework.test.context.support.internalDynamicPropertyRegistrarBeanInitializer@ExtendWith(SpringExtension.class):让Spring TestContext Framework与JUnit关联,其中最大的作用就是注册了一系列TestExecutionListener,包括DependencyInjectionTestExecutionListener;DependencyInjectionTestExecutionListener:依赖注入监听器,主要作用让测试类实例拥有像 Spring Bean 一样的“超能力”(注入和回调),尽管测试类本身并不是一个 Bean。字段与方法注入 (
@Autowired/@Value):这是最常用的功能。它会扫描测试类中的字段和方法(包括private字段),并利用容器中的 Bean 填充它们。- 实现原理:它调用了容器内部的
beanFactory.autowireBeanProperties()。
- 实现原理:它调用了容器内部的
触发 Aware 回调接口:刚才实验发现的,如果实现了
ApplicationContextAware,是这个监听器通过手动判断接口类型,然后调用setApplicationContext把容器赋值给setApplicationContext()方法参数的。- 覆盖范围:支持
EnvironmentAware,ResourceLoaderAware,ApplicationEventPublisherAware,MessageSourceAware等。
- 覆盖范围:支持
GenericApplicationContext:从结果中我们可以看到,获取到的容器是一个最基本的GenericApplicationContext容器;并且打印其中的Bean,会发现都是一些最基本的处理器,各个处理器的作用见Spring 容器与Bean
通过以上的案例,我们可以知道,默认情况下,Spring Test会提供一个最基本的容器。
我们可以通过@ContextConfiguration,在测试类上注册Bean,有以下方式:
组件类(Component Classes)
组件类是指符合以下特征的类:
- 类上标注了
@Configuration注解; - 被特定注解(如
@Component,@Service,@Repository等)标记的类; - 一个符合 JSR-330 的类,并带有
jakarta.inject注解; - 任何包含
@Bean方法的类; - 任何其他要被注册为Bean的类(也就是没有任何注解的普通类);
可以使用@ContextConfiguration的classed属性来指定组件类,例如:
java
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {Bean1.class})
public class Demo02 implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Test
public void test() {
System.out.println(applicationContext);
System.out.println("-------------------------------------");
Arrays.stream(applicationContext.getBeanDefinitionNames()).forEach(System.out::println);
}
}
class Bean1{
@Bean
public Bean2 bean2(){
return new Bean2();
}
}
class Bean2{
}结果中就会有Bean1和Bean2两个组件了。
XML配置文件
除了使用组件类来配置容器,还可以使用XML配置文件,使用@ContextConfiguration的locations属性,此处不再详细介绍。
初始化器(Initializer)
我们可以实现ApplicationContextInitializer接口,自己注册Bean,然后在ContextConfiguration中通过initializers指定自定义的初始化器,例如:
java
@ExtendWith(SpringExtension.class)
@ContextConfiguration(initializers = {MyInitializer.class})
public class Demo02 implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Test
public void test() {
System.out.println(applicationContext);
System.out.println("-------------------------------------");
Arrays.stream(applicationContext.getBeanDefinitionNames()).forEach(System.out::println);
}
}
class MyInitializer implements ApplicationContextInitializer<GenericApplicationContext> {
@Override
public void initialize(GenericApplicationContext applicationContext) {
applicationContext.registerBean("bean1", Bean1.class);
applicationContext.registerBean("bean2", Bean2.class);
}
}
class Bean1{
}
class Bean2{
}2.2 Profile
我们可以在测试类上,通过@ActiveProfiles注解,来指明启用的Profile,例如:
java
@ExtendWith(SpringExtension.class)
@ActiveProfiles("test")
@ContextConfiguration(classes = {Demo03Configuration.class})
public class Demo03 {
@Autowired
private ApplicationContext applicationContext;
@Test
void test(){
Arrays.stream(applicationContext.getBeanDefinitionNames()).forEach(System.out::println);
}
}
class Demo03Configuration{
@Bean
@Profile("test")
public Bean1 demo03Bean1(){
return new Bean1();
}
@Bean
@Profile("prod")
public Bean2 demo03Bean2(){
return new Bean2();
}
}可以发现,容器中只有Bean1。
2.3 环境配置
我们先看看@PropertySource的使用。
首先,新建src/test/resources/myProp.properties文件,内容如下:
properties
name=zs
place=test
path=xxx然后,配合@Configuration,使用@PropertySource将myProp.properties中的配置引入容器环境中:
java
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {Demo04Configuration.class})
public class Demo04 {
@Autowired
private Environment environment;
@Test
void test(){
StandardEnvironment standardEnvironment = (StandardEnvironment) environment;
MutablePropertySources propertySources = standardEnvironment.getPropertySources();
propertySources.stream()
.forEach(x->{
System.out.println(x);
if ("class path resource [myProp.properties]".equals(x.getName())){
System.out.println(x.getSource());
}
});
}
}
@Configuration
@PropertySource(value = "classpath:myProp.properties")
class Demo04Configuration {
}结果如下:
txt
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
ResourcePropertySource {name='class path resource [myProp.properties]'}
{path=xxx, name=zs, place=test}可以看到,容器环境中现在有三类配置:
- 系统属性(System Properties): JVM 启动参数,比如
java -jar myapp.jar -Dserver.port=8081中的server.port。 - 操作系统环境变量(Environment Variables): 操作系统级别的变量,例如
PATH、JAVA_HOME。 - 配置文件(Configuration Files): 这是最常见的来源,比如
application.properties、application.yml或通过@PropertySource加载的自定义文件。Spring 会将这些文件的内容解析为键值对,并添加到Environment中。
从上到下,优先级从高到低,也就是说当 Spring 需要查找一个属性(例如 ${db.name})时,它会按照顺序依次询问每一个 PropertySource,一旦找到就立即返回,不再继续往下找。
在测试中,我们可以使用@TestPropertySource,为测试类引入新的配置,主要有两种方式:
- 通过
locations或value属性:指定配置文件文件; - 通过
properties属性:直接在代码中指定配置,通过键值对的方式;
例如,再新建src/test/resources/myTestProp.properties,内容如下:
properties
name=ls
place=testPropertySource
path=yyy然后在测试代码中通过@TestPropertySource引入:
java
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {Demo04Configuration.class})
@TestPropertySource(
locations = "classpath:myTestProp.properties",
properties = {
"name:ww",
"place bbbbb",
"path=aaa"
}
)
public class Demo04 {
@Autowired
private Environment environment;
@Value("${path}")
private String path;
@Value("${place}")
private String place;
@Value("${name}")
private String name;
@Test
void test(){
StandardEnvironment standardEnvironment = (StandardEnvironment) environment;
MutablePropertySources propertySources = standardEnvironment.getPropertySources();
propertySources.stream()
.forEach(System.out::println);
}
@Test
public void test01() {
System.out.println("name:" + name);
System.out.println("place:" + place);
System.out.println("path:" + path);
}
}
@Configuration
@PropertySource(value = "classpath:myProp.properties")
class Demo04Configuration {
}从test()的结果:
txt
MapPropertySource {name='Inlined Test Properties'}
ResourcePropertySource {name='class path resource [myTestProp.properties]'}
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
ResourcePropertySource {name='class path resource [myProp.properties]'}会发现通过@TestPropertySource中的properties属性指定的配置,优先级最高,通过@TestPropertySource中的locations属性指定的配置优先级次之。
从test01()的结果:
txt
name:ww
place:bbbbb
path:aaa也可验证优先级顺序,并且通过@TestPropertySource中的properties属性指定配置支持三种格式:
key=valuekey:valuekey value
Spring Test还支持Dynamic Property Sources
2.4 容器缓存
在测试中,启动一个 ApplicationContext(Spring 容器)通常非常耗时。为了加速测试,Spring 会将加载过的容器缓存起来。如果后续的测试类声明了完全相同的配置,Spring 就会复用缓存中的容器,而不是重新加载。
Spring 会根据一组配置参数生成一个唯一的 Cache Key。只有当以下所有参数完全一致时,才会命中缓存:
locations(@ContextConfiguration中设置)classes(@ContextConfiguration中设置)contextInitializerClasses(@ContextConfiguration中设置)contextCustomizers(ContextCustomizerFactory) :ContextCustomizer是 Spring 提供的**“容器魔改器”**。每当使用一些特殊的测试注解来改变容器内部的 Bean 结构或属性时,Spring 就会生成一个特定的ContextCustomizer。由于这些“魔改”直接改变了容器的本质,Spring 必须把它们也算进缓存的 Key 里。@DynamicPropertySource会影响缓存的key, bean重写 (例如@TestBean,@MockitoBean,@MockitoSpyBean等等),以及其他来自Spring Boot Test的支持也可能影响缓存的key。contextLoader(@ContextConfiguration中设置)parent(@ContextHierarchy中设置)activeProfiles(@ActiveProfiles中设置)propertySourceDescriptors(@TestPropertySource中设置)propertySourceProperties(@TestPropertySource中设置 )resourceBasePath(@WebAppConfiguration中设置)
容器缓存策略:
默认大小:缓存最大容量默认为 32。
淘汰策略:使用 LRU(最近最少使用) 算法。当达到上限时,最久未使用的容器会被自动关闭并移出缓存。
自定义大小:可以通过 JVM 参数 spring.test.context.cache.maxSize 进行调整。
缓存的限制:进程与进程间不共享
- 缓存是存储在静态变量中的。
- 这意味着缓存只能在同一个 JVM 进程内生效。如果使用maven-surefire-plugin开启了多个JVM,那么同一个容器可能在不同的JVM都会创建。
如何强制清理缓存?(@DirtiesContext,详细查看容器重置一节)
如果某个测试方法或测试类修改了容器的状态(例如修改了单例 Bean 的内部属性或重新注册了 Bean),该容器就不再是“干净”的,可能会影响后续测试。
- 解决办法:在类或方法上标注
@DirtiesContext。 - 作用:告诉 Spring 运行完该测试后,立即将当前容器从缓存中移除并关闭,下次需要时重新加载。
下面的例子验证容器缓存的存在:
java
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {CommonConfiguration.class})
public class Demo05 {
@Autowired
private ApplicationContext applicationContext;
@Test
void test(){
System.out.println(applicationContext.getId());
}
}然后新建Demo06测试类,除了类名,其他全部相同。
然后在IDEA中同时选择Demo05和Demo06,同时运行,结果如下:

可以看到容器ID是相同的,说明使用的是同一个容器,缓存成功。
3. TestExecutionListener
3.1 介绍
TestExecutionListener用于增强测试过程,也就是说,该接口提供了很多时间节点,用来在测试过程中进行扩展。
java
public interface TestExecutionListener {
// 在@BeforeAll之前执行
default void beforeTestClass(TestContext testContext) throws Exception {
}
// 在测试类构造函数之后执行
default void prepareTestInstance(TestContext testContext) throws Exception {
}
// 在@BeforeEach之前执行
default void beforeTestMethod(TestContext testContext) throws Exception {
}
// 在测试方法之前执行
default void beforeTestExecution(TestContext testContext) throws Exception {
}
// 在测试方法之后执行
default void afterTestExecution(TestContext testContext) throws Exception {
}
// 在@AfterEach之后执行
default void afterTestMethod(TestContext testContext) throws Exception {
}
// 在@AfterAll之后执行
default void afterTestClass(TestContext testContext) throws Exception {
}
}在Spring Test中,提供了很多内置的TestExecutionListener实现,顺序如下:
ServletTestExecutionListener:当使用@WebAppConfiguration时,它负责配置 Web 容器的模拟对象(如MockHttpServletRequest,MockHttpSession)。它确保在测试执行时,线程上下文中存在这些 Mock 对象。DirtiesContextBeforeModesTestExecutionListener:处理@DirtiesContext注解中配置为“测试前”(Before)模式的情况。在测试运行前销毁并重建上下文。ApplicationEventsTestExecutionListener:如果在测试中需要验证某个业务逻辑是否发布了特定的ApplicationEvent,这个监听器会记录这些事件,并允许通过ApplicationEventsAPI 进行断言。BeanOverrideTestExecutionListener:支持 Spring 的 Bean Overriding 机制(如@TestBean)。它允许在测试中用模拟实现或特定定义的 Bean 替换容器中原有的 Bean。DependencyInjectionTestExecutionListener:这是最核心的监听器。它负责在测试实例创建后,根据配置对测试类中的@Autowired或@Inject成员进行依赖注入。没有它,测试类里的 Bean 都是null。MicrometerObservationRegistryTestExecutionListener:为 Micrometer 的可观测性提供支持。它确保在测试期间ObservationRegistry正确配置,以便可以测试指标采样或链路追踪逻辑。DirtiesContextTestExecutionListener:处理@DirtiesContext常见的“测试后”(After)模式。如果测试方法标记了此注解,它会在执行完后清空缓存,确保下一个测试类使用全新的上下文。CommonCachesTestExecutionListener:负责清理ApplicationContext中的各种内部资源缓存。这通常用于防止由于缓存导致的内存泄漏或跨测试的状态干扰。TransactionalTestExecutionListener:让测试具有事务性。它会在测试开始前开启事务,并在结束后默认回滚(Rollback),保证测试产生的数据不会污染数据库。SqlScriptsTestExecutionListener:处理@Sql注解。它负责在指定的生命周期点执行 SQL 脚本(如初始化表数据或清理现场)。EventPublishingTestExecutionListener:它会将测试执行的生命周期(如beforeTestClass,afterTestMethod等)作为 ApplicationEvent 发布到 Spring 容器中。这样可以编写自定义的@EventListener来监听测试进程,而不是非要实现一个 TEL 接口。MockitoResetTestExecutionListener:当使用@MockitoBean或@MockitoSpyBean定义 Mock 时,这个监听器负责在每个测试方法执行后自动调用Mockito.reset()。这防止了上一个测试的打桩(Stubbing)或验证信息影响到下一个测试。
大部分情况下,使用Spring Test提供的默认实现就可以满足测试要求,但有时也需要自定义实现。
首先,我们可以实现TestExecutionListener接口,然后在不同的测试过程中定义自己的逻辑:
java
public class MyTestExecutionListener implements TestExecutionListener {
@Override
public void beforeTestClass(TestContext testContext) throws Exception {
System.out.println("beforeTestClass");
}
@Override
public void prepareTestInstance(TestContext testContext) throws Exception {
System.out.println("prepareTestInstance");
}
@Override
public void beforeTestMethod(TestContext testContext) throws Exception {
System.out.println("beforeTestMethod");
}
@Override
public void beforeTestExecution(TestContext testContext) throws Exception {
System.out.println("beforeTestExecution");
}
@Override
public void afterTestExecution(TestContext testContext) throws Exception {
System.out.println("afterTestExecution");
}
@Override
public void afterTestMethod(TestContext testContext) throws Exception {
System.out.println("afterTestMethod");
}
@Override
public void afterTestClass(TestContext testContext) throws Exception {
System.out.println("afterTestClass");
}
}然后在测试类上,使用@TestExecutionListeners来引用自定义的监听器:
java
@ExtendWith(SpringExtension.class)
@TestExecutionListeners(value = MyTestExecutionListener.class)
public class Demo07 {
public Demo07(){
System.out.println("Demo07");
}
@BeforeAll
public static void beforeAll() {
System.out.println("beforeAll");
}
@AfterAll
public static void afterAll() {
System.out.println("afterAll");
}
@BeforeEach
public void beforeEach() {
System.out.println("beforeEach");
}
@AfterEach
public void afterEach() {
System.out.println("afterEach");
}
@Test
public void test() {
System.out.println("test");
}
}结果如下:
txt
beforeTestClass
beforeAll
Demo07
prepareTestInstance
beforeTestMethod
beforeEach
beforeTestExecution
test
afterTestExecution
afterEach
afterTestMethod
afterAll
afterTestClass从中就可以看出各个方法作用的时间点。
默认情况下,如果使用@TestExecutionListeners引用自定义的监听器,那么Spring Test默认的监听器就不会注册到该测试类了,如有需要,可以设置mergeMode属性:
MergeMode.REPLACE_DEFAULTS:默认值,表示替代默认的监听器;MergeMode.MERGE_WITH_DEFAULTS:合并默认值;
java
@ExtendWith(SpringExtension.class)
@TestExecutionListeners(
value = MyTestExecutionListener.class,
mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
)
public class Demo07 {
//...
}如果只有很少的情况需要自定义监听器,那么使用@TestExecutionListeners很方便,但是,如果很多测试类都需要自定义监听器,那么使用注解就显得很麻烦了,此时,Spring Test提供了一种自动注册监听器的方式。
首先,在类路径下新建META_INF/spring.factories文件,然后,以org.springframework.test.context.TestExecutionListener为key,自定义监听器全限定类名为value:
properties
org.springframework.test.context.TestExecutionListener=org.example.springtest.MyTestExecutionListener如果有多个,以英文逗号分隔。这也是Spring Test注册默认监听器的方式。
如果自定义的监听器需要排序,那么可以实现Ordered接口,或者使用@Order注解,数字越小排在越前面。
接下来就介绍几个重要的监听器。
3.2 依赖注入
依赖注入功能是DependencyInjectionTestExecutionListener监听器提供的,主要实现了两个方法
prepareTestInstance:在测试类实例创建完成后,对字段进行依赖注入;beforeTestMethod:在测试方法执行前,再次进行依赖注入,但是一般情况下不会再次注入;
我们可以通过@Autowired或@Inject进行依赖注入,根据注解的位置,可以使用字段注入或Setter注入。
在 Spring 的依赖注入逻辑中,@Autowired 的查找策略确实遵循 “先根据类型 (Type),再根据名称 (Name)” 的顺序。如果想调整注入的Bean,可以使用@Qualifier。
例如:
java
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {Demo08Config.class})
public class Demo08 {
// 字段注入
@Autowired
private Demo08Bean zs;
private Demo08Bean ls;
// setter注入
@Autowired
public void setLs(Demo08Bean ls){
this.ls = ls;
}
@Test
public void test(){
System.out.println(zs);
System.out.println(ls);
}
}
@Configuration
class Demo08Config{
@Bean
public Demo08Bean zs(){
return new Demo08Bean("zs");
}
@Bean
public Demo08Bean ls(){
return new Demo08Bean("ls");
}
}
@Data
@AllArgsConstructor
class Demo08Bean{
private String name;
}CAUTION
注意,如果使用JUnit Jupiter,也可以使用构造方法注入和测试方法参数注入。
但是,这不是DependencyInjectionTestExecutionListener提供的功能,而是SpringExtension提供的,因为SpringExtension实现了ParameterResolver接口。
例如:
java
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {Demo08Config.class})
public class Demo08 {
private Demo08Bean zs;
private Demo08Bean ls;
@Autowired
public Demo08(Demo08Bean zs, Demo08Bean ls){
this.zs = zs;
this.ls = ls;
}
@Test
public void test(@Autowired Demo08Bean ww){
System.out.println(zs);
System.out.println(ls);
System.out.println(ww);
}
}
@Configuration
class Demo08Config{
@Bean
public Demo08Bean zs(){
return new Demo08Bean("zs");
}
@Bean
public Demo08Bean ls(){
return new Demo08Bean("ls");
}
@Bean
public Demo08Bean ww(){
return new Demo08Bean("ww");
}
}
@Data
@AllArgsConstructor
class Demo08Bean{
private String name;
}3.3 事务管理
在Spring TestContext Framework中,事务管理功能是由TransactionalTestExecutionListener监听器提供的,事务管理功能起作用的前提是在容器中存在PlatformTransactionManager实现,另外,如果要实现事务管理,还需要在测试类或测试方法上加上@Transactional注解。
3.3.1 基本案例
本小节以最基本的案例演示在Spring Test中,事务管理的功能。
首先定义容器:
java
@Configuration
public class DataConfig {
// 1. 配置数据源 (DataSource)
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setDriverClassName("com.mysql.cj.jdbc.Driver");
config.setJdbcUrl("jdbc:mysql://localhost:3306/employees?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true");
config.setUsername("root");
config.setPassword("xxx");
// 连接池优化配置
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.setMaximumPoolSize(10);
return new HikariDataSource(config);
}
// 2. 配置事务管理器 (PlatformTransactionManager)
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
// 对于 JDBC 或 MyBatis,使用 DataSourceTransactionManager
// 如果是 Hibernate,则需要使用 HibernateTransactionManager
return new DataSourceTransactionManager(dataSource);
}
// 3. 配置JdbcTemplate,用于操作数据库
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}然后在测试类中使用@Transactional用于事务管理:
java
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = DataConfig.class)
public class TransactionDemo01 {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
@Transactional
void test(){
jdbcTemplate.execute("insert into stu(name) values('xx'),('yy')");
}
}运行测试,会发现数据库并没有增加数据。
说明事务自动回滚了,这就是Spring Test中事务管理的基本功能,自动回滚Spring Test事务,以免测试数据污染数据库,易造成后续测试的失败。
3.3.2 @Transactional
在Spring Test中,如果在测试方法上使用@Transactional注解,那么该测试方法会在结束时回滚数据库操作,并且测试生命周期方法(@BeforeEach、@AfterEach)也会被包含在测试事务中。
如果在测试类上使用@Transactional注解,那么该测试类中的所有测试方法都会运行在事务中,并且默认回滚。如果要让某个测试方法不运行在事务中,那么可以将propagation属性设置为NOT_SUPPORTED 或 NEVER,例如:
java
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = DataConfig.class)
@Transactional
public class TransactionDemo02 {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
void test(){
jdbcTemplate.execute("insert into stu(name) values('aa'),('bb')");
}
@Test
@Transactional(propagation = Propagation.NEVER)
void test01(){
jdbcTemplate.execute("insert into stu(name) values('00'),('11')");
}
}查询数据库,发现00和11两条数据插入到了数据库。
如果在测试方法的事务中执行业务逻辑,而业务逻辑又开启了事务(称为spring-managed transactions或application-managed transactions,统称为业务事务),那么视业务事务的propagation而定:
- 业务事务的propagation为
Propagation.REQUIRES_NEW,那么业务事务会在另一个数据库连接中执行,并且会提交到数据库中,最终测试事务回滚时,该部分数据不会被回滚; - 业务事务的propagation为
REQUIRED或SUPPORTS,业务事务会加入测试事务,会一起回滚;
如果在容器中有多个PlatformTransactionManager实现,那么可以通过transactionManager属性来指定实现,例如;@Transactional(transactionManager = "myTxMgr")。
默认情况下,事务自动回滚,如果我们想让事务提交,可以使用@Commit注解,例如:
java
@Test
@Transactional
@Commit
void test(){
jdbcTemplate.execute("insert into stu(name) values('aa'),('bb')");
}同理,也可以使用@Rollback来回滚事务(这是默认行为,所以@Rollback很少用到)。
3.3.3 事务外的执行方法
默认情况下,@BeforeEach和@AfterEach都会在事务中执行,如果想在事务前或事务后执行方法,Spring Test提供了@BeforeTransaction和@AfterTransaction注解,用来支持该需求。
例如:
java
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = DataConfig.class)
public class TransactionDemo01 {
@Autowired
private JdbcTemplate jdbcTemplate;
@BeforeTransaction
void beforeTransaction(TestInfo testInfo){
System.out.println(testInfo);
jdbcTemplate.execute("insert into stu(name) values('a')");
}
@BeforeEach
void beforeEach(){
jdbcTemplate.execute("insert into stu(name) values('b')");
}
@Test
@Transactional
void test(){
jdbcTemplate.execute("insert into stu(name) values('c')");
}
@AfterEach
void afterEach(){
jdbcTemplate.execute("insert into stu(name) values('d')");
}
@BeforeTransaction
void afterTransaction(){
jdbcTemplate.execute("insert into stu(name) values('e')");
}
}查询数据库,发现 a和e 两条记录成功插入数据库。
默认情况下,@BeforeTransaction 和 @AfterTransaction 标注的方法在任何事务方法执行前后都会生效,如果只想让某些事务方法生效,可以使用TestInfo参数,Jupiter会自动注入当前执行的测试信息,我们可以根据测试信息来选择性执行某段逻辑。
3.4 执行SQL脚本
在集成测试中,如果能在测试方法前后执行SQL脚本,那么对于测试就会更简单。
3.4.1 基本案例
在Spring Test中,@Sql 用于在测试方法执行之前或之后执行 SQL 脚本。它通常用于初始化数据库状态(如插入测试数据)或在测试结束后清理现场。
例如,先新建文件src/test/resources/sql/demo.sql,内容如下:
sql
delete from stu;
insert into stu(name) values('zs');然后测试类:
java
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = DataConfig.class)
public class SqlDemo01 {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
@Sql(scripts = "classpath:sql/demo.sql")
void test(){
String sql = "select * from stu";
RowMapper<Stu> stuRowMapper = (rs, rowNum) -> {
Stu stu = new Stu();
stu.setId(rs.getInt("id"));
stu.setName(rs.getString("name"));
return stu;
};
List<Stu> stus = jdbcTemplate.query(sql, stuRowMapper);
Assertions.assertEquals(1, stus.size());
Assertions.assertEquals("zs", stus.get(0).getName());
}
}
@Data
class Stu{
private Integer id;
private String name;
}测试通过,说明@Sql中指定的脚本文件执行了。
3.4.2 @Sql
@Sql如果指定在测试方法上,那么只对该测试方法有效;如果@Sql指定在测试类上,那么对该类中的所有测试方法有效。
如果在测试类和测试方法上都有@Sql,那么默认情况下,测试方法的@Sql会覆盖掉测试类上的@Sql(除了executionPhase设置为BEFORE_TEST_CLASS或AFTER_TEST_CLASS的@Sql),我们可以设置@SqlMergeMode,用来修改覆盖行为:
@SqlMergeMode(SqlMergeMode.MergeMode.MERGE):合并执行,也就是说先执行测试类上的脚本,再执行测试方法上的脚本;@SqlMergeMode(SqlMergeMode.MergeMode.OVERRIDE):覆盖执行,只执行测试方法上的脚本;
@Sql可以重复写:
java
@Test
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`"))
@Sql("/test-user-data.sql")
void userTest() {
// run code that uses the test schema and test data
}也可以用@SqlGroup包裹:
java
@Test
@SqlGroup({
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
@Sql("/test-user-data.sql")
})
void userTest() {
// run code that uses the test schema and test data
}两者效果完全一样,推荐使用后者。
@Sql有很多属性可以设置,本小节就来介绍一下:
value或scripts:用来指定SQL脚本路径;statements:直接在注解中指定SQL语句,会在SQL脚本执行之后执行;如果
scripts/value和statements都没有指定,那么Spring Test有一个默认的脚本寻找算法:@Sql注解在测试类上,如果测试类为com.example.MyTest,那么默认的脚本路径为classpath:com/example/MyTest.sql;@Sql注解在测试方法上,如果测试类为com.example.MyTest,测试方法为testMethod(),那么默认的脚本路径为classpath:com/example/MyTest.testMethod.sql;
executionPhase:SQL执行时机,可选值如下:BEFORE_TEST_METHOD:在测试方法前执行,默认值;AFTER_TEST_METHOD:在测试方法后执行;BEFORE_TEST_CLASS:在测试类前执行,也就是说和@BeforeAll相同,注意,使用该值,需要@Sql在测试类上,并且无法回滚;AFTER_TEST_CLASS:在测试类后执行,也就是说和@AfterAll相同,注意,使用该值,需要@Sql在测试类上,并且无法回滚;
config:具体设置,类型为SqlConfig,子配置项如下:dataSource:数据源名称;transactionManager:事务管理器名称;transactionMode:事务模式,即 SQL 脚本执行时如何处理事务隔离,取值如下:DEFAULT:默认值,如果当前已经存在一个“测试管理事务”(比如在测试方法上加了
@Transactional),脚本就会在该事务内运行。如果当前没有活跃事务,它的行为就等同于
INFERRED。INFERRED:推断模式它会尝试从 Spring 上下文中寻找
PlatformTransactionManager。如果找到了,就开启一个事务来执行脚本并提交。
如果没找到,就直接在非事务环境下执行(即每条 SQL 语句执行完立即生效)。
ISOLATED:强制开启一个事务无论当前测试方法是否开启了事务,脚本都会在一个全新的、独立的物理事务中执行。
脚本执行完毕后会立即提交(Commit)。
重要后果:因为它是独立提交的,所以即使测试方法标记了
@Transactional并在最后回滚了,ISOLATED模式下执行的 SQL 产生的数据依然会留在数据库中。如果指定事务模式为ISOLATED,但是容器中没有
PlatformTransactionManager,会报错如下:cannot execute SQL scripts using Transaction Mode [ISOLATED] without a PlatformTransactionManager.
encoding:SQL脚本编码,默认取平台编码;separator:SQL语句分分隔符,默认为英文分号;;commentPrefix和commentPrefixs:单行注释前缀,默认为--;blockCommentStartDelimiter和blockCommentEndDelimiter:块注释起止符,分别为/*和*/;errorMode:决定了当 SQL 脚本执行过程中发生错误(异常)时,测试框架该如何反应,可选值如下:FAIL_ON_ERROR:一旦脚本中任何一条 SQL 语句执行失败(例如违反约束、语法错误、表不存在),立即抛出异常并终止后续 SQL 的执行。CONTINUE_ON_ERROR:如果某条 SQL 语句报错,它会打印一条错误日志,然后跳过该错误,继续执行脚本中的下一条语句。IGNORE_FAILED_DROPS:这是一种“智能型”的容错模式。它只忽略针对DROP语句的错误,而对于INSERT、UPDATE、CREATE等语句的错误仍然会触发FAIL_ON_ERROR。
3.4.3 和事务的关系
默认情况下,SQL脚本执行功能是由SqlScriptsTestExecutionListener监听器提供的,其优先级为5000:
java
public class SqlScriptsTestExecutionListener extends AbstractTestExecutionListener implements AotTestExecutionListener {
public static final int ORDER = 5000;
}而在TransactionalTestExecutionListener监听器中,优先级为4000:
java
public class TransactionalTestExecutionListener extends AbstractTestExecutionListener {
public static final int ORDER = 4000;
}说明是按照如下流程:
- 先执行
TransactionalTestExecutionListener.beforeTestMethod(),开启事务; - 再执行
SqlScriptsTestExecutionListener.beforeTestMethod(),根据配置选择加入现有事务或者开启新事务;
org.springframework.test.context.TestContextManager#afterTestMethod()中,获取的是逆序的监听器getReversedTestExecutionListeners():
java
for (TestExecutionListener testExecutionListener : getReversedTestExecutionListeners()) {
try {
testExecutionListener.afterTestMethod(getTestContext());
}
catch (Throwable ex) {
logException(ex, callbackName, testExecutionListener, testInstance, testMethod);
if (afterTestMethodException == null) {
afterTestMethodException = ex;
}
else {
afterTestMethodException.addSuppressed(ex);
}
}
}即表示:
- 先执行
SqlScriptsTestExecutionListener.afterTestMethod(),此时仍然在事务中,或者新开事务; - 再执行
TransactionalTestExecutionListener.afterTestMethod(),回滚或提交事务;
3.5 容器重置
容器重置主要是指在测试运行前或运行后,将缓存的容器删除,并重新创建容器,主要涉及的注解是@DirtiesContext,主要起作用的监听器是DirtiesContextBeforeModesTestExecutionListener和DirtiesContextTestExecutionListener。
@DirtiesContext表示测试关联的容器已被修改,监听器会将对应的容器从缓存中删除,因此,后续有测试需要键相同的容器时,容器会重建。该注解可以用在测试类和测试方法上,主要属性如下:
classMode:当@DirtiesContext注解在测试类上时,可以设置类模式,可选值如下:BEFORE_CLASS:在测试类运行之前设置脏容器,因此,在测试类启动之前,会先摧毁容器,然后新建容器;AFTER_CLASS:在测试类运行之后设置脏容器,默认值;BEFORE_EACH_TEST_METHOD:在每个测试方法运行之前设置脏容器;AFTER_EACH_TEST_METHOD:在每个测试方法运行之后设置脏容器;
methodMode:当@DirtiesContext注解在测试方法上时,可以设置方法模式,可选值如下:BEFORE_METHOD:在测试方法运行之前设置脏容器;AFTER_METHOD:在测试方法运行之后设置脏容器,默认值;
如果测试类和测试方法上都有@DirtiesContext,那么都会生效,也就是说,如果在类上的设置classMode为BEFORE_EACH_TEST_METHOD,在测试方法上设置methodMode为AFTER_METHOD,那么该容器在测试方法前会失效并重建(因为方法运行需要容器),在测试方法运行之后也会失效。
案例如下:
java
@ExtendWith(SpringExtension.class)
@ContextConfiguration(initializers = {MyInitializer.class})
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
public class DirtiesContextDemo {
@Test
@Order(1)
@DirtiesContext
public void test1() {
System.out.println("test1");
}
@Test
@Order(2)
public void test2() {
System.out.println("test2");
}
}
class MyInitializer implements ApplicationContextInitializer<GenericApplicationContext> {
public void initialize(GenericApplicationContext applicationContext) {
System.out.println("MyInitializer");
applicationContext.registerBean("bean", Bean.class);
}
}
class Bean{
@PreDestroy
public void cleanup() {
System.out.println("容器正在关闭,正在释放资源...");
}
}结果如下:
txt
MyInitializer
容器正在关闭,正在释放资源...
MyInitializer
test1
容器正在关闭,正在释放资源...
MyInitializer
容器正在关闭,正在释放资源...
MyInitializer
test2
容器正在关闭,正在释放资源...第1行和第10行,是因为整个测试运行之前和运行之后,需要新建和销毁容器。
第2-5行,是运行测试方法test1时,由于在类级别上标注了classMpde=BEFORE_EACH_TEST_METHOD的脏容器注解,因此认为容器脏了,销毁容器(第2行),然后运行test1方法需要容器,因此又新建容器(第3行),然后运行test1方法(第4行),最后由于test1方法上的@DirtiesContext,又销毁容器(第5行)。
第6-9行,在运行测试方法test2之前,发现没有容器,因此新建容器(第6行),之后由于类级别上的@DirtiesContext,销毁容器(第7行),然后运行测试方法test2之前需要容器,又新建容器(第8行),最后运行测试方法test2(第9行)。
整个例子比较复杂,并且发现容器的创建与销毁非常频繁,建议使用@DirtiesContext还是要非常慎重。
3.6 Bean覆盖
Bean覆盖核心元注解是@BeanOverride,由BeanOverrideTestExecutionListener监听器处理,作用是:
检查冲突:寻找容器中是否存在同类型或同名的
BeanDefinition。存在时:替换(Replace),删除旧的
BeanDefinition,塞入新的BeanDefinition。不存在时:新增(Add),直接在
BeanDefinitionRegistry中注册一个新的定义。
@BeanOverride是元注解,在使用中,我们通常使用子注解:@TestBean、@MockitoBean和@MockitoSpyBean。
3.6.1 @TestBean
通用的Bean覆盖,通过静态方法来提供新的BeanDefinition,该方法不依赖任何框架(如Mockito)。常用属性如下:
methodName:用于指定提供 Bean 实例的静态方法名称。如果不指定methodName,Spring 会尝试在当前类中寻找一个和字段名称相同的静态方法。name:如果想替换一个特定名称的 Bean(当容器里有多个同类型 Bean 时),可以用这个属性指定 Bean 的 ID。或者可以使用@Qualifier。enforceOverride:如果容器中不存在同类型或同名的Bean,那么是否抛异常。默认值为false,表示新增,不抛异常;如果设置为true,则抛异常。
案例如下:
java
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {TestBeanDemoConfig.class})
public class TestBeanDemo {
@TestBean(name = "bean01")
public Bean01 testBean;
@Autowired
private ApplicationContext applicationContext;
@Test
public void test() {
Arrays.stream(applicationContext.getBeanDefinitionNames()).forEach(System.out::println);
System.out.println("--------");
System.out.println(testBean);
System.out.println("--------");
System.out.println(applicationContext.getBean("bean01"));
}
static Bean01 testBean(){
return new Bean01("ww");
}
}
class TestBeanDemoConfig{
@Bean
public Bean01 bean01(){
return new Bean01("zs");
}
@Bean
public Bean01 bean02(){
return new Bean01("ls");
}
}
@AllArgsConstructor
@Data
class Bean01{
private String name;
}结果如下:
txt
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
testBeanDemoConfig
org.springframework.test.context.support.internalDynamicPropertyRegistrarBeanInitializer
bean01
bean02
--------
Bean01(name=ww)
--------
Bean01(name=ww)可以发现bean01被成功覆盖了。
3.6.2 @MockitoBean 和 @MockitoSpyBean
@MockitoBean 和 @MockitoSpyBean都是和Mockito框架绑定的注解,分别使用mock()和spy()创建出来的模拟对象,来覆盖原始Bean定义。
同样的,@MockitoBean 和 @MockitoSpyBean会先根据类型来推断要覆盖的Bean,如果存在多个类型相同的Bean,@Qualifier或name属性可以用来唯一确定要覆盖的Bean,如果都没指定,那么字段名称将作为最后的Bean名称。
@MockitoBean使用REPLACE_OR_CREATE策略来覆盖或新增Bean,也就是说,如果能在容器中找到要覆盖的Bean,那就覆盖原始Bean,如果找不到就新增,该策略可以通过enforceOverride来调整。
@MockitoSpyBean使用WRAP策略,也就是说,如果原始Bean不存在,将会报错。
默认情况,在测试方法执行后,会由MockitoResetTestExecutionListener监听器对模拟对象执行reset(),重置模拟对象,重置时间可以通过reset属性来设置,可选值如下:
AFTER:在测试方法执行之后重置模拟对象,默认值;BEFORE:在测试方法执行之前重置模拟对象;NONE:不重置;
注意,如果使用@TestBean然后手动创建Mockito模拟对象,MockitoResetTestExecutionListener不会生效。
3.6.3 注意事项
使用@TestBean、@MockitoBean 和 @MockitoSpyBean会修改容器的唯一键,导致容器缓存失效,因此,为了避免容器反复创建,推荐在基类中使用模拟对象:
java
// 这样配置,A 和 B 就会共享容器
abstract class BaseTest {
@TestBean(methodName = "sharedMock")
protected MyService myService;
@MockitoBean
protected DemoService demoService;
@MockitoSpyBean
protected EmailService emailService;
static MyService sharedMock() {
return new MyFakeService();
}
}
class TestA extends BaseTest { @Test void t() {} }
class TestB extends BaseTest { @Test void t() {} }4. MockMvc
4.1 ServletContext
在介绍WebApplicationContext之前,我们先了解一下ServletContext。
ServletContext是 Java Web(Servlet 规范)中的一个全局上下文对象,用于在整个 Web 应用范围内共享数据和资源。可以把它理解为:整个 Web 应用的“运行环境容器”或“全局作用域”。
ServletContext与Spring 无关,是由 Servlet 容器(例如Tomcat)在 Web 应用启动时创建,流程如下:
txt
启动 Tomcat
↓
部署应用(war / 上下文)
↓
创建 ServletContext
↓
加载 web.xml / 初始化配置
↓
创建 Servlet / Filter / Listener一个Web应用就有一个ServletContext:
txt
Tomcat
├── app1 → ServletContext #1
├── app2 → ServletContext #2ServletContext的主要功能如下:
- 全局配置与参数共享:
ServletContext像是一个巨大的全局Map,它在整个 Web 应用的生命周期内持续存在。- 存储全局属性:通过
setAttribute(name, value),可以存放一些全应用共享的对象。在 Spring 出现之前,这是唯一的全局共享方案。 - 读取配置参数:它可以读取在WEB容器级别配置的
context-param,这些参数对所有的 Servlet 和 Filter 都是可见的。
- 存储全局属性:通过
- 资源定位与文件访问:它是应用与服务器文件系统之间的桥梁。
- 获取真实路径:通过
getRealPath("/"),它可以把一个虚拟的 Web 路径(如/uploads)转换为服务器磁盘上的绝对路径。 - 加载资源流:使用
getResourceAsStream("/WEB-INF/config.xml")可以直接读取类路径或 Web 目录下的资源,而不必担心部署环境的差异。
- 获取真实路径:通过
- Web 容器信息的窗口:它记录了当前 Web 应用运行时的“户口信息”:
- 上下文路径 (Context Path):比如项目访问地址是
http://localhost:8080/api/v1,那么/api/v1就是由ServletContext管理的。 - 服务器信息:可以查询当前运行的是哪个版本的 Tomcat、Jetty 或 Undertow。
- Servlet API 版本:了解当前WEB容器支持的最高 Servlet 规范版本。
- 上下文路径 (Context Path):比如项目访问地址是
- 动态注册组件:这是 Spring Boot 能够“脱离
web.xml配置文件”运行的关键。- 动态添加:它允许在应用启动时,通过代码动态地注册
Servlet、Filter或Listener。 - Spring 的对接点:Spring 的
DispatcherServlet正是通过ServletContext提供的接口,把自己塞进 Web 容器里去拦截请求的。
- 动态添加:它允许在应用启动时,通过代码动态地注册
- 生命周期管理:
ServletContext有两个重要的生命周期事件,常用于执行初始化或销毁动作:contextInitialized:Web应用启动时触发。contextDestroyed:应用停止或重载时触发,常用于释放数据库连接池、关闭线程池等。
4.2 WebApplicationContext
在 Spring 体系中,ApplicationContext 是顶层核心接口,而 WebApplicationContext 是专门为 Web 开发定制的子接口。
WebApplicationContext相比ApplicationContext,有以下区别:
| 特性 | 传统 ApplicationContext | WebApplicationContext |
|---|---|---|
| 主要用途 | Java SE 应用、Swing、命令行工具 | Spring MVC / Spring Boot Web 应用 |
| 环境依赖 | 独立运行,无需 Servlet 容器 | 必须运行在 Servlet 容器(如 Tomcat)中 |
| 与容器的关系 | 它是唯一的容器 | 它与 ServletContext 绑定 |
| Bean 作用域 | singleton, prototype | 额外支持 request, session, application, websocket |
| 资源加载 | 基于类路径或文件系统 | 支持基于 Web 根目录(ServletContext)加载 |
关键增强点详解:
可以获取
ServletContext:这是两者最本质的区别。WebApplicationContext定义了一个方法getServletContext()。这意味着 Spring 容器可以随时访问 Servlet 标准中的全局对象,从而获取上下文路径(Context Path)、读取
WEB-INF下的资源或处理临时目录。扩展的 Bean 作用域 (Scopes):在非 Web 应用中,Bean 只有单例和多例。但在
WebApplicationContext中,Spring 引入了与 HTTP 请求生命周期绑定的作用域:Request:每个 HTTP 请求创建一个新实例。
Session:每个用户会话创建一个新实例。
Application:整个 Web 应用生命周期内一个实例(存放在
ServletContext中)。特殊的初始化机制
传统方式:通常手动
new ClassPathXmlApplicationContext(...)。Web 方式:不需要手动 new。在 Spring Boot 中,它由嵌入式 Tomcat 启动时自动创建;在传统 War 包中,由
ContextLoaderListener监听ServletContext的启动事件来触发创建。javapublic class ContextLoaderListener extends ContextLoader implements ServletContextListener { // 在Servlet Context 启动时触发,创建WebApplicationContext @Override public void contextInitialized(ServletContextEvent event) { ServletContext scToUse = getServletContextToUse(event); initWebApplicationContext(scToUse); } // 在Servlet Context 关闭时触发,关闭WebApplicationContext @Override public void contextDestroyed(ServletContextEvent event) { ServletContext scToUse = getServletContextToUse(event); closeWebApplicationContext(scToUse); ContextCleanupListener.cleanupAttributes(scToUse); } // 获取Servlet Context private ServletContext getServletContextToUse(ServletContextEvent event) { return (this.servletContext != null ? this.servletContext : event.getServletContext()); } }在
initWebApplicationContext(scToUse)中,有以下代码:javaservletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.rootContext); // wac 是 WebApplicationContext,sc 是 Servlet Context wac.setServletContext(sc);可以发现
WebApplicationContext和ServletContext是相互引用的关系。
4.3 测试中的WAC
在Spring Test中,如果要加载WebApplicationContext,可以在测试类上加上@WebAppConfiguration注解,Spring TestContext Framework会创建一个MockServletContext(模拟的ServletContext,并没有启动Web容器),并提供给WebApplicationContext。
默认情况下,MockServletContext的资源路径设置为src/main/webapp,如果要更改,可以设置@WebAppConfiguration的value属性,例如:
java
@WebAppConfiguration("src/test/webapp")这是相对于项目根路径的路径,如果要改为类路径,那么可以使用classpath:前缀,例如:
java
@WebAppConfiguration("classpath:/webapp")下面展示一个基本的WebApplicationContext单元测试:
首先按照以下示意图创建项目结构:

测试类如下:
java
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration("classpath:/webapp")
public class WacDemo {
@Autowired
private WebApplicationContext wac;
@Test
void test(){
// webApplicationContext 容器
System.out.println("wac: " + wac);
// 从webApplicationContext 获取ServletContext
ServletContext servletContext = wac.getServletContext();
System.out.println("servletContext: " + servletContext);
// 从ServletContext 获取webApplicationContext
System.out.println("-----------------");
System.out.println(servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE));
System.out.println("-----------------");
// 获取ServletContext中的资源,/表示根路径下的资源
Set<String> resourcePaths = servletContext.getResourcePaths("/");
resourcePaths.forEach(System.out::println);
}
}结果如下:
txt
wac: org.springframework.web.context.support.GenericWebApplicationContext@5911e990
servletContext: org.springframework.mock.web.MockServletContext@58b71ceb
-----------------
org.springframework.web.context.support.GenericWebApplicationContext@5911e990
-----------------
/home.html
/img/4.4 MockMvc
4.4.1 介绍
MockMvc提供了Spring MVC应用测试支持,它不启动WEB应用,通过模拟Request,测试从请求到响应的全流程,包括请求映射、请求解析、参数校验、异常处理、响应消息转换等等。
在实际的Web应用中,有一个Web容器,它监听着一个端口。当请求发送到到该端口后,Web容器根据contextPath,将请求转发给不同的web应用,在该应用中,根据servlet mapping 规则,将请求转发给不同的Servlet:
txt
Tomcat(监听 8080)
├── app1 → contextPath: /app1
└── app2 → contextPath: /app2txt
客户端请求
↓
端口(8080)
↓
Tomcat Connector(接收请求)
↓
根据 contextPath 定位 Web 应用(/app1)
↓
进入对应 ServletContext
↓
FilterChain
↓
根据 URL pattern 匹配 Servlet(/user) --> 在Spring MVC 中,就是DispatcherServlet
↓
调用 Servlet.service() --> 到不同的 Controller而在MockMvc中,并没有启动Web容器,模拟请求是直接调用DispatcherServlet中的方法,然后进行后续的动作。
java
MockMvc.perform()
↓
MockHttpServletRequest
↓
Mock FilterChain(可选)
↓
DispatcherServlet ← 核心入口(仍然存在)
↓
Controller4.4.2 配置
在Spring MVC中,配置MockMvc分为两种方式:
- WebApplicationContext:加载完整的Spring MVC 配置(Spring MVC相关组件类,如HandlerMapping(请求->处理器匹配器)、HandlerAdapter(处理器执行适配器)、参数解析器、类型转换器、内容协商等等);
- Standalone:一次只测试单个Controller,不会加载Spring MVC配置,需要手动注入;
在一般情况下,一般使用WebApplicationContext方式来配置MockMvc,以下案例也主要介绍该方式。
配置方式如下:
jade
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration(classes = MockMvcDemoConfig.class)
public class MockMvcDemo {
private MockMvc mockMvc;
@BeforeEach
void beforeEach(@Autowired WebApplicationContext wac){
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
}
@ComponentScan("org.example.controller")
@EnableWebMvc
@Configuration
class MockMvcDemoConfig {
}除了简单配置MockMvc,我们还可以加上一些其他配置,例如:
加上过滤器Filter:
java
@BeforeEach
void beforeEach(@Autowired WebApplicationContext wac){
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac)
.addFilter(new MyFilter())
.build();
}
// 定义过滤器
class MyFilter implements Filter{
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("MyFilter");
chain.doFilter(request, response);
}
}统一添加结果处理器:
java
@BeforeEach
void beforeEach(@Autowired WebApplicationContext wac){
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac)
.addFilter(new MyFilter())
.alwaysDo(MockMvcResultHandlers.print())
.build();
}MockMvcResultHandlers.print()将请求结果打印在控制台,通常用于调试;
统一校验结果:
java
@BeforeEach
void beforeEach(@Autowired WebApplicationContext wac){
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac)
.addFilter(new MyFilter())
.alwaysDo(MockMvcResultHandlers.print())
.alwaysExpect(MockMvcResultMatchers.status().isOk())
.build();
}- 校验结果始终返回200,如果不是返回200,则抛异常;
统一设置请求:例如请求头
java
@BeforeEach
void beforeEach(@Autowired WebApplicationContext wac){
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac)
.addFilter(new MyFilter())
.alwaysDo(MockMvcResultHandlers.print())
.alwaysExpect(MockMvcResultMatchers.status().isOk())
.defaultRequest(
MockMvcRequestBuilders.get("/")
.accept(MediaType.APPLICATION_JSON_VALUE)
.characterEncoding(StandardCharsets.UTF_8)
.header("Authorization", "Bearer xxx")
)
.build();
}相当于设置了默认值,后续测试中实际发起请求时,如果没有设置相同的请求参数,则使用默认值。
4.4.3 基本案例
下面介绍使用MockMvc来测试Spring MVC。
首先,在org.example.controller包下定义Controller:
java
@RestController
public class WacDemoController {
@PostMapping("/demo/{version}")
public String demo(@PathVariable("version")
@Min(value = 1, message = "version不能小于1")
Integer version,
@RequestParam("name") String name) {
return "demo:" + version + " " + name;
}
}注意,使用了参数校验
@Min,需要添加以下依赖:xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
然后编写测试类:
java
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration(classes = MockMvcDemoConfig.class)
public class MockMvcDemo {
private MockMvc mockMvc;
@BeforeEach
void beforeEach(@Autowired WebApplicationContext wac){
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac)
.addFilter(new MyFilter())
.alwaysDo(MockMvcResultHandlers.print())
.alwaysExpect(MockMvcResultMatchers.status().isOk())
.defaultRequest(
MockMvcRequestBuilders.get("/")
.accept(MediaType.APPLICATION_JSON_VALUE)
.characterEncoding(StandardCharsets.UTF_8)
.header("Authorization", "Bearer xxx")
)
.build();
}
@Test
void test() throws Exception {
MvcResult mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.post("/demo/1?name=zs")).andReturn();
System.out.println(mvcResult.getResponse().getStatus());
System.out.println(mvcResult.getResponse().getContentAsString());
}
}
@ComponentScan("org.example.controller")
@EnableWebMvc
@Configuration
class MockMvcDemoConfig {
}
class MyFilter implements Filter{
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("MyFilter");
chain.doFilter(request, response);
}
}结果如下:
txt
MyFilter
MockHttpServletRequest:
HTTP Method = POST
Request URI = /demo/1
Parameters = {name=[zs]}
Headers = [Accept:"application/json", Authorization:"Bearer xxx"]
Body = null
Session Attrs = {}
Handler:
Type = org.example.controller.WacDemoController
Method = org.example.controller.WacDemoController#demo(Integer, String)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json", Content-Length:"9"]
Content type = application/json
Body = demo:1 zs
Forwarded URL = null
Redirected URL = null
Cookies = []
200
demo:1 zs第1行是过滤器打印的。
第3-38行是MockMvcResultHandlers.print()打印的。
第39,40行是测试方法中打印的。
4.4.4 MockMvcTester
MockMvcTester与AssertJ整合,提供了更流畅的断言能力。
需要在类路径下有AssertJ。
基本案例如下:
java
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration(classes = MockMvcTesterDemoConfig.class)
public class MockMvcTesterDemo {
private MockMvcTester mockMvcTester;
public MockMvcTesterDemo(WebApplicationContext wac) {
mockMvcTester = MockMvcTester.from(wac);
}
@Test
public void test() {
mockMvcTester.post()
.uri("/demo/1?name=zs")
.assertThat()
.hasStatus(HttpStatus.OK)
.bodyText()
.isEqualTo("demo:1 zs");
}
}
@ComponentScan("org.example.controller")
@EnableWebMvc
@Configuration
class MockMvcTesterDemoConfig {
}MockMvcTester也可以配置,例如配置消息转换器:
java
private MockMvcTester mockMvcTester;
private ObjectMapper objectMapper;
public MockMvcTesterDemo(WebApplicationContext wac) {
mockMvcTester = MockMvcTester.from(wac)
.withHttpMessageConverters(
List.of(new MappingJackson2HttpMessageConverter())
);
objectMapper = new ObjectMapper();
}
@Test
void test_stu() throws JsonProcessingException {
mockMvcTester.post()
.uri("/stu")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(new StuVo("zs", 18)))
.assertThat()
.hasStatus(HttpStatus.OK)
.bodyJson()
.convertTo(StuVo.class)
.satisfies(stu -> {
assertThat(stu).extracting("name").isEqualTo("zs");
assertThat(stu).extracting("age").isEqualTo(18);
});
}在结果第23行时,使用配置的消息转换器,将json转换为对象。
4.5 RestTestClient
RestTestClient是从Spring Framework 7.0之后引入的,用于测试接口的工具。
详细文档:https://docs.spring.io/spring-framework/reference/testing/resttestclient.html
设置:
java
RestTestClient client;
@BeforeEach
void setUp(WebApplicationContext context) { // Inject the configuration
// Create the `RestTestClient`
client = RestTestClient.bindToApplicationContext(context).build();
}使用:
java
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody();5. 其他注解
5.1 @SpringJUnitConfig
@SpringJUnitConfig是一个组合注解,整合了@ExtendWith(SpringExtension.class)以及@ContextConfiguration,因此在测试类上,可以只标注一个注解:
java
@SpringJUnitConfig(TestConfig.class)
class ConfigurationClassJUnitJupiterSpringTests {
// class body...
}同理,@SpringJUnitWebConfig在@SpringJUnitConfig的基础上,又组合了@WebAppConfiguration。
参考资料
[1] Spring Test 官方文档:https://docs.spring.io/spring-framework/reference/testing.html
附录
1. 单元测试、集成测试、端到端测试
单元测试定义
单元测试是针对程序中最小可测试单元进行的测试,通常是一个函数或一个类的方法。
特点:
- 只测试 一个类或方法
- 不依赖外部系统
- 依赖通常使用 mock / stub
- 运行速度很快
常见工具:
- JUnit
- Mockito
集成测试定义
集成测试是验证多个组件组合在一起是否能够正确协作。
例如测试:
- Service + Repository
- Repository + Database
- Controller + Service
- Spring 容器 + Bean
端到端测试定义
端到端测试是从系统入口到系统出口,对完整业务流程进行验证的测试。
也就是说测试会经过:
用户接口 → Web层 → 业务层 → 数据层 → 外部系统整个系统链路。
举个例子,假设现在有一个UserService:
java
public class UserService {
private UserRepository repository;
public UserService(UserRepository repository){
this.repository = repository;
}
public User getUser(int id){
return repository.findById(id);
}
}在单元测试中,UserRepository使用mock对象,只测试UserService方法逻辑是否正常:
java
@Test
void testGetUser(){
UserRepository repo = Mockito.mock(UserRepository.class);
Mockito.when(repo.findById(1)).thenReturn(new User("Tom"));
UserService service = new UserService(repo);
User user = service.getUser(1);
assertEquals("Tom", user.getName());
}在测试过程中,不会访问数据库、不会启动 Spring、不会调用网络。
在集成测试中,会启动Spring,注入真实 Bean,访问数据库:
java
@SpringBootTest
class UserServiceTest {
@Autowired
UserService userService;
@Test
void testGetUser(){
User user = userService.getUser(1);
assertEquals("Tom", user.getName());
}
}使用了@SpringBootTest,这是Spring Boot Test中的注解。
假设假设有一个用户注册功能,系统结构:
Browser
↓
Controller
↓
Service
↓
Repository
↓
Database
↓
Email Service端到端测试会验证完整流程:
- 打开注册页面
- 输入用户名和密码
- 点击注册
- 系统保存用户
- 发送确认邮件
- 页面显示注册成功
测试的是:
整个业务流程是否工作在 Spring Boot 项目中常见 E2E 测试:
启动整个应用
java
@SpringBootTest(webEnvironment = RANDOM_PORT)
class AppTest {
}然后通过 HTTP 调用:
java
TestRestTemplate restTemplate;测试:
java
ResponseEntity<String> response =
restTemplate.getForEntity("/users", String.class);这样实际上测试的是:
txt
HTTP → Controller → Service → Repository → Database除了在Spring Boot中进行端到端测试,还有WEB E2E(End to End,端到端)工具:
- Selenium
- Cypress
- Playwright
例如使用 Selenium:
java
driver.get("http://localhost:8080/register");
driver.findElement(By.id("username")).sendKeys("tom");
driver.findElement(By.id("password")).sendKeys("123456");
driver.findElement(By.id("submit")).click();
assertTrue(driver.getPageSource().contains("success"));这里测试的是:
浏览器 → Web服务器 → 后端 → 数据库三种测试对比:
| 类型 | 测试范围 | 是否启动系统 | 速度 |
|---|---|---|---|
| 单元测试 | 一个类/方法 | 否 | 快 |
| 集成测试 | 多个组件 | 部分 | 中 |
| 端到端测试 | 整个系统 | 是 | 慢 |
在一个项目中,推荐各个测试比例如下:
txt
70% 单元测试
20% 集成测试
10% 端到端测试