在 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
第一个测试类对 OrderIdGenerator
的 queue
进行操作:
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
第二个测试类也对 OrderIdGenerator
的 queue
进行操作:
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. 问题现象
- 如果
TestClassA
和TestClassB
共享同一个OrderIdGenerator
实例(默认情况下,Spring Boot 的@SpringBootTest
会复用上下文),它们会共享同一个ConcurrentLinkedQueue<String>
。 - 当
TestClassA
和TestClassB
并行或顺序执行时,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. 解决后的执行流程
-
TestClassA
执行:- Spring 销毁现有的上下文(如果存在),并重新加载一个新的上下文。
TestClassA
使用一个全新的OrderIdGenerator
实例。- 测试完成后,Spring 不会复用该上下文。
-
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...");
}
}
执行流程:
- 在执行
test1
和test2
之前,Spring 会销毁现有的上下文(如果存在),并重新加载一个新的上下文。 test1
和test2
会共享同一个上下文实例。- 如果有其他测试类运行,它们不会复用
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...");
}
}
-
执行流程:
test1
和test2
会共享同一个上下文实例。- 在
test2
执行完成后,Spring 会销毁上下文。 - 如果有其他测试类运行,它们会加载一个新的上下文。
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
:在每个测试方法执行之后销毁上下文。