@SpringBootTest @DirtiesContext

在 Spring Boot 测试中,多个测试类共享同一个 Spring 应用上下文(ApplicationContext),如果这些测试类对同一个对象的共享状态(如 ConcurrentLinkedQueue<String>)进行操作,就可能导致上下文污染(Context Pollution)。这种污染会导致测试之间相互干扰,从而引发数据不一致的问题,例如在使用 assertEquals 时出现预期值和实际值不匹配的情况。

通过在测试类上添加 @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS),可以强制 Spring 在每个测试类执行之前销毁当前上下文并重新加载一个新的上下文,从而避免上下文污染的问题。


示例代码

1. 共享的 OrderIdGenerator

这是一个包含 ConcurrentLinkedQueue<String> 的类,模拟共享状态的情况:

java 复制代码
import java.util.concurrent.ConcurrentLinkedQueue;

public class OrderIdGenerator {
    private final ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();

    public void addId(String id) {
        queue.offer(id);
    }

    public String getId() {
        return queue.poll();
    }

    public int getQueueSize() {
        return queue.size();
    }
}
2. 测试类 A

第一个测试类对 OrderIdGeneratorqueue 进行操作:

java 复制代码
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest
public class TestClassA {

    @Autowired
    private OrderIdGenerator orderIdGenerator;

    @Test
    void testAddAndGetId() {
        orderIdGenerator.addId("ID-A1");
        orderIdGenerator.addId("ID-A2");

        assertEquals("ID-A1", orderIdGenerator.getId());
        assertEquals("ID-A2", orderIdGenerator.getId());
    }
}
3. 测试类 B

第二个测试类也对 OrderIdGeneratorqueue 进行操作:

java 复制代码
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest
public class TestClassB {

    @Autowired
    private OrderIdGenerator orderIdGenerator;

    @Test
    void testAddAndGetId() {
        orderIdGenerator.addId("ID-B1");
        orderIdGenerator.addId("ID-B2");

        assertEquals("ID-B1", orderIdGenerator.getId());
        assertEquals("ID-B2", orderIdGenerator.getId());
    }
}
4. 问题现象
  • 如果 TestClassATestClassB 共享同一个 OrderIdGenerator 实例(默认情况下,Spring Boot 的 @SpringBootTest 会复用上下文),它们会共享同一个 ConcurrentLinkedQueue<String>
  • TestClassATestClassB 并行或顺序执行时,queue 的状态可能被另一个测试类修改,导致 assertEquals 失败。

5. 解决方案

在每个测试类上添加 @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS),强制 Spring 在每个测试类执行之前销毁上下文并重新加载:

修改后的 TestClassA
java 复制代码
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;

import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
public class TestClassA {

    @Autowired
    private OrderIdGenerator orderIdGenerator;

    @Test
    void testAddAndGetId() {
        orderIdGenerator.addId("ID-A1");
        orderIdGenerator.addId("ID-A2");

        assertEquals("ID-A1", orderIdGenerator.getId());
        assertEquals("ID-A2", orderIdGenerator.getId());
    }
}
修改后的 TestClassB
java 复制代码
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;

import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
public class TestClassB {

    @Autowired
    private OrderIdGenerator orderIdGenerator;

    @Test
    void testAddAndGetId() {
        orderIdGenerator.addId("ID-B1");
        orderIdGenerator.addId("ID-B2");

        assertEquals("ID-B1", orderIdGenerator.getId());
        assertEquals("ID-B2", orderIdGenerator.getId());
    }
}
6. 解决后的执行流程
  1. TestClassA 执行

    • Spring 销毁现有的上下文(如果存在),并重新加载一个新的上下文。
    • TestClassA 使用一个全新的 OrderIdGenerator 实例。
    • 测试完成后,Spring 不会复用该上下文。
  2. TestClassB 执行

    • Spring 再次销毁上下文,并重新加载一个新的上下文。
    • TestClassB 使用一个全新的 OrderIdGenerator 实例,与 TestClassA 的实例完全隔离。

总结

问题原因
  • Spring Boot 测试默认会复用上下文(ApplicationContext),多个测试类共享同一个 Bean 实例(如 OrderIdGenerator)。
  • 如果这些测试类对共享状态(如 ConcurrentLinkedQueue<String>)进行操作,就可能导致上下文污染,进而引发测试失败。
解决方法
  • 使用 @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS),确保每个测试类使用独立的上下文实例。
适用场景
  • 测试类之间存在共享状态,且这些状态可能被修改时。
  • 测试类需要完全隔离的上下文环境。

@DirtiesContext

@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS) 是 Spring 提供的一个注解,用于标记测试类或测试方法会修改 Spring 应用上下文(ApplicationContext),从而需要在特定时机销毁并重新加载上下文。


1. @DirtiesContext 的作用

  • 核心功能

    • 告诉 Spring 测试框架,当前测试类或测试方法会对应用上下文造成污染(例如修改了 Bean 的状态、环境配置等)。
    • 在测试完成后,Spring 会销毁当前的应用上下文,并在需要时重新加载一个新的上下文。
  • 使用场景

    • 当测试对上下文的状态进行了修改,可能影响其他测试时,使用 @DirtiesContext 可以确保上下文的隔离性。

    • 例如:

      • 修改了某些单例 Bean 的状态。
      • 动态注册或移除了 Bean。
      • 修改了环境变量或系统属性。

2. classMode 属性

  • classMode 属性用于指定上下文销毁的时机,适用于测试类级别的上下文管理。

  • 可选值

    • DirtiesContext.ClassMode.AFTER_CLASS(默认值):

      • 在测试类执行完成后销毁上下文。
      • 适用于测试类之间需要隔离上下文的场景。
    • DirtiesContext.ClassMode.BEFORE_CLASS

      • 在测试类执行之前销毁上下文,并重新加载一个新的上下文。
      • 适用于测试类需要一个全新的上下文实例的场景。
    • DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD

      • 在每个测试方法执行完成后销毁上下文。
      • 适用于测试方法之间需要隔离上下文的场景。
    • DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD

      • 在每个测试方法执行之前销毁上下文,并重新加载一个新的上下文。
      • 适用于测试方法需要一个全新的上下文实例的场景。

3. @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS) 的含义

  • 含义

    • 在测试类执行之前,销毁当前的应用上下文(如果存在),并重新加载一个新的上下文。
    • 确保测试类运行时使用的是一个全新的上下文实例。
  • 适用场景

    • 当测试类需要一个全新的上下文实例,而不能复用之前的上下文时。

    • 例如:

      • 测试类需要动态修改环境配置或系统属性。
      • 测试类需要重新初始化某些单例 Bean。

4. 示例代码

4.1 使用 @DirtiesContext 的测试类
java 复制代码
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;

@SpringBootTest
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
public class MyTestClass {

    @Test
    void test1() {
        System.out.println("Running test1...");
    }

    @Test
    void test2() {
        System.out.println("Running test2...");
    }
}
执行流程
  1. 在执行 test1test2 之前,Spring 会销毁现有的上下文(如果存在),并重新加载一个新的上下文。
  2. test1test2 会共享同一个上下文实例。
  3. 如果有其他测试类运行,它们不会复用 MyTestClass 的上下文。

4.2 与 AFTER_CLASS 的对比

如果将 classMode 设置为 AFTER_CLASS(默认值),上下文的销毁时机会发生变化:

java 复制代码
@SpringBootTest
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
public class MyTestClass {
    @Test
    void test1() {
        System.out.println("Running test1...");
    }

    @Test
    void test2() {
        System.out.println("Running test2...");
    }
}
  • 执行流程

    1. test1test2 会共享同一个上下文实例。
    2. test2 执行完成后,Spring 会销毁上下文。
    3. 如果有其他测试类运行,它们会加载一个新的上下文。

5. 注意事项

5.1 上下文销毁的开销
  • 销毁和重新加载上下文是一个相对昂贵的操作,可能会增加测试的执行时间。
  • 因此,@DirtiesContext 应该只在必要时使用。
5.2 与上下文缓存的关系
  • Spring 测试框架会缓存应用上下文,以便在多个测试类中复用。
  • 使用 @DirtiesContext 会使当前上下文从缓存中移除,强制重新加载。
5.3 与并行测试的关系
  • 如果启用了并行测试(例如 Gradle 的 maxParallelForks 设置大于 1),@DirtiesContext 的使用可能会导致上下文的竞争或不一致。
  • 在并行测试中,尽量避免频繁销毁上下文。

6. 总结

  • @DirtiesContext

    • 用于标记测试会污染上下文,Spring 会在指定时机销毁并重新加载上下文。
  • classMode = BEFORE_CLASS

    • 在测试类执行之前销毁上下文,并重新加载一个新的上下文。
    • 适用于测试类需要一个全新的上下文实例的场景。
  • 与其他模式的对比

    • BEFORE_CLASS:在测试类执行之前销毁上下文。
    • AFTER_CLASS(默认):在测试类执行之后销毁上下文。
    • BEFORE_EACH_TEST_METHOD:在每个测试方法执行之前销毁上下文。
    • AFTER_EACH_TEST_METHOD:在每个测试方法执行之后销毁上下文。
相关推荐
码代码的小农4 小时前
Resilience4j与Spring Cloud Gateway整合指南:构建弹性的API网关
spring boot
angushine4 小时前
Gateway获取下游最终响应码
java·开发语言·gateway
爱的叹息4 小时前
关于 JDK 中的 jce.jar 的详解,以及与之功能类似的主流加解密工具的详细对比分析
java·python·jar
一一Null5 小时前
Token安全存储的几种方式
android·java·安全·android studio
AUGENSTERN_dc5 小时前
RaabitMQ 快速入门
java·后端·rabbitmq
晓纪同学5 小时前
C++ Primer (第五版)-第十三章 拷贝控制
java·开发语言·c++
小样vvv5 小时前
【源码】SpringMvc源码分析
java
nzwen6665 小时前
Redis学习笔记及总结
java·redis·学习笔记
燃星cro5 小时前
参照Spring Boot后端框架实现序列化工具类
java·spring boot·后端