Skip to content

Spring Test

本文介绍Spring Test框架。Spring Test框架为单元测试和集成测试都提供了支持,本文主要关注集成测试。

关于什么是单元测试,什么是集成测试,以及两者的区别,参考附录1。

1. Spring Test原理

1.1 关键组件

Spring Test对集成测试提供的支持,主要在于Spring TestContext Framework。

Spring TestContext Framework由TestContextManagerTestContextTestExecutionListenerContextLoader四大模块组成。

  • TestContextManager:每个测试类都有一个对应的TestContextManager,用于管理测试执行过程,重要的两个组成部分就是TestContextTestExecutionListener

  • 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可以如图所示:

image-20260314190910140

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框架产生联系。

image-20260314191853689

例如,在SpringExtensionbeforeAll()实现中:

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的类(也就是没有任何注解的普通类);

可以使用@ContextConfigurationclassed属性来指定组件类,例如:

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配置文件,使用@ContextConfigurationlocations属性,此处不再详细介绍。

初始化器(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,使用@PropertySourcemyProp.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): 操作系统级别的变量,例如 PATHJAVA_HOME
  • 配置文件(Configuration Files): 这是最常见的来源,比如 application.propertiesapplication.yml 或通过 @PropertySource 加载的自定义文件。Spring 会将这些文件的内容解析为键值对,并添加到 Environment 中。

从上到下,优先级从高到低,也就是说当 Spring 需要查找一个属性(例如 ${db.name})时,它会按照顺序依次询问每一个 PropertySource一旦找到就立即返回,不再继续往下找。

在测试中,我们可以使用@TestPropertySource,为测试类引入新的配置,主要有两种方式:

  • 通过locationsvalue属性:指定配置文件文件;
  • 通过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=value
  • key:value
  • key 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,同时运行,结果如下:

image-20260315164954015

可以看到容器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,这个监听器会记录这些事件,并允许通过 ApplicationEvents API 进行断言。
  • 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_SUPPORTEDNEVER,例如:

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为 REQUIREDSUPPORTS ,业务事务会加入测试事务,会一起回滚;

如果在容器中有多个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')");
    }

}

查询数据库,发现 ae 两条记录成功插入数据库。

默认情况下,@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_CLASSAFTER_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有很多属性可以设置,本小节就来介绍一下:

  • valuescripts:用来指定SQL脚本路径;

  • statements:直接在注解中指定SQL语句,会在SQL脚本执行之后执行;

    如果scripts/valuestatements都没有指定,那么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语句分分隔符,默认为英文分号;

    • commentPrefixcommentPrefixs:单行注释前缀,默认为--

    • blockCommentStartDelimiterblockCommentEndDelimiter:块注释起止符,分别为/**/

    • errorMode:决定了当 SQL 脚本执行过程中发生错误(异常)时,测试框架该如何反应,可选值如下:

      • FAIL_ON_ERROR:一旦脚本中任何一条 SQL 语句执行失败(例如违反约束、语法错误、表不存在),立即抛出异常并终止后续 SQL 的执行。
      • CONTINUE_ON_ERROR:如果某条 SQL 语句报错,它会打印一条错误日志,然后跳过该错误,继续执行脚本中的下一条语句。
      • IGNORE_FAILED_DROPS:这是一种“智能型”的容错模式。它只忽略针对 DROP 语句的错误,而对于 INSERTUPDATECREATE 等语句的错误仍然会触发 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;
}

说明是按照如下流程:

  1. 先执行TransactionalTestExecutionListener.beforeTestMethod(),开启事务;
  2. 再执行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);
    }
  }
}

即表示:

  1. 先执行SqlScriptsTestExecutionListener.afterTestMethod(),此时仍然在事务中,或者新开事务;
  2. 再执行TransactionalTestExecutionListener.afterTestMethod(),回滚或提交事务;

3.5 容器重置

容器重置主要是指在测试运行前或运行后,将缓存的容器删除,并重新创建容器,主要涉及的注解是@DirtiesContext,主要起作用的监听器是DirtiesContextBeforeModesTestExecutionListenerDirtiesContextTestExecutionListener

@DirtiesContext表示测试关联的容器已被修改,监听器会将对应的容器从缓存中删除,因此,后续有测试需要键相同的容器时,容器会重建。该注解可以用在测试类和测试方法上,主要属性如下:

  • classMode:当@DirtiesContext注解在测试类上时,可以设置类模式,可选值如下:
    • BEFORE_CLASS:在测试类运行之前设置脏容器,因此,在测试类启动之前,会先摧毁容器,然后新建容器;
    • AFTER_CLASS:在测试类运行之后设置脏容器,默认值;
    • BEFORE_EACH_TEST_METHOD:在每个测试方法运行之前设置脏容器;
    • AFTER_EACH_TEST_METHOD:在每个测试方法运行之后设置脏容器;
  • methodMode:当@DirtiesContext注解在测试方法上时,可以设置方法模式,可选值如下:
    • BEFORE_METHOD:在测试方法运行之前设置脏容器;
    • AFTER_METHOD:在测试方法运行之后设置脏容器,默认值;

如果测试类和测试方法上都有@DirtiesContext,那么都会生效,也就是说,如果在类上的设置classModeBEFORE_EACH_TEST_METHOD,在测试方法上设置methodModeAFTER_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,@Qualifiername属性可以用来唯一确定要覆盖的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 #2

ServletContext的主要功能如下:

  • 全局配置与参数共享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 规范版本。
  • 动态注册组件:这是 Spring Boot 能够“脱离 web.xml 配置文件”运行的关键。
    • 动态添加:它允许在应用启动时,通过代码动态地注册 ServletFilterListener
    • Spring 的对接点:Spring 的 DispatcherServlet 正是通过 ServletContext 提供的接口,把自己塞进 Web 容器里去拦截请求的。
  • 生命周期管理ServletContext 有两个重要的生命周期事件,常用于执行初始化或销毁动作:
    • contextInitialized:Web应用启动时触发。
    • contextDestroyed:应用停止或重载时触发,常用于释放数据库连接池、关闭线程池等。

4.2 WebApplicationContext

在 Spring 体系中,ApplicationContext 是顶层核心接口,而 WebApplicationContext 是专门为 Web 开发定制的子接口。

WebApplicationContext相比ApplicationContext,有以下区别:

特性传统 ApplicationContextWebApplicationContext
主要用途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 的启动事件来触发创建。

    java
    public 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)中,有以下代码:

    java
    servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.rootContext);
    
    // wac 是 WebApplicationContext,sc 是 Servlet Context
    wac.setServletContext(sc);

    可以发现WebApplicationContextServletContext是相互引用的关系。

4.3 测试中的WAC

在Spring Test中,如果要加载WebApplicationContext,可以在测试类上加上@WebAppConfiguration注解,Spring TestContext Framework会创建一个MockServletContext(模拟的ServletContext,并没有启动Web容器),并提供给WebApplicationContext

默认情况下,MockServletContext的资源路径设置为src/main/webapp,如果要更改,可以设置@WebAppConfigurationvalue属性,例如:

java
@WebAppConfiguration("src/test/webapp")

这是相对于项目根路径的路径,如果要改为类路径,那么可以使用classpath:前缀,例如:

java
@WebAppConfiguration("classpath:/webapp")

下面展示一个基本的WebApplicationContext单元测试:

首先按照以下示意图创建项目结构:

image-20260320163023139

测试类如下:

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: /app2
txt
客户端请求

端口(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   ← 核心入口(仍然存在)

Controller

4.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

端到端测试会验证完整流程:

  1. 打开注册页面
  2. 输入用户名和密码
  3. 点击注册
  4. 系统保存用户
  5. 发送确认邮件
  6. 页面显示注册成功

测试的是:

整个业务流程是否工作

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% 端到端测试