Spring TestContext 框架 - @ContextConfiguration、切片测试与集成测试
概述
Spring TestContext 框架是 Spring Framework 提供的一套测试基础设施,位于 spring-test 模块中。它在 JUnit(4/5)等测试框架之上构建了一层上下文管理能力,使得测试类可以自动加载 Spring ApplicationContext、依赖注入、事务管理、缓存上下文等。理解 TestContext 框架对编写高质量的企业级集成测试至关重要。
本文基于 Spring Framework 5.3.x / Spring Boot 3.x 进行源码分析,所有代码示例均可在相应版本中运行。
1. TestContext 框架核心概念
TestContext 框架有四大核心抽象:
| 组件 | 职责 |
|---|---|
TestContext | 封装当前测试的上下文元数据(测试类、测试实例、实际方法、配置属性等) |
TestContextManager | 测试生命周期的编排者,负责在测试执行前后调用 TestExecutionListener |
TestExecutionListener | 扩展点接口,在测试生命周期的不同阶段(before/beforeClass/after/afterClass)执行回调 |
ContextCache | 缓存已加载的 ApplicationContext,避免每个测试类重复加载 |
1.1 TestContext
TestContext 的核心字段如下:
// org.springframework.test.context.TestContext
public class TestContext {
private final ContextCache contextCache; // 上下文缓存
private final String testClassName; // 测试类名
private final String[] activeProfiles; // 激活的 profile
private final Set<Class<?>> propertySourceLocations; // 属性源位置
private final ContextCustomizer contextCustomizers; // 上下文定制器
private volatile ApplicationContext applicationContext; // 缓存的 ApplicationContext
private volatile Object testInstance; // 当前测试实例
private volatile Method testMethod; // 当前测试方法
private volatile Throwable testException; // 测试异常
}关键方法 getApplicationContext() 实现了延迟加载 + 缓存:
public ApplicationContext getApplicationContext() {
ApplicationContext context = this.applicationContext;
if (context == null) {
synchronized (this) {
context = this.applicationContext;
if (context == null) {
// 从 ContextCache 中获取或创建
context = ContextLoaderUtils.loadContext(this);
this.applicationContext = context;
}
}
}
return context;
}1.2 TestContextManager
TestContextManager 是测试框架与 Spring 之间的桥梁。每个测试类对应一个 TestContextManager 实例。
public class TestContextManager {
private final TestContext testContext;
private final List<TestExecutionListener> testExecutionListeners;
public TestContextManager(Class<?> testClass) {
this.testContext = new TestContext(testClass, new DefaultContextCache());
// SPI 加载所有 TestExecutionListener
this.testExecutionListeners = new ArrayList<>();
TestExecutionListenerUtils.loadListeners(this.testExecutionListeners, testClass);
}
// 在 @BeforeEach 中调用
public void beforeTestMethod(Object testInstance, Method testMethod) throws Exception {
// 遍历所有 Listener,按 @Order 排序后依次执行
for (TestExecutionListener listener : this.testExecutionListeners) {
listener.beforeTestMethod(this.testContext);
}
}
// 在 @AfterEach 中调用
public void afterTestMethod(Object testInstance, Method testMethod, Throwable exception) throws Exception {
for (TestExecutionListener listener : this.testExecutionListeners) {
listener.afterTestMethod(this.testContext);
}
}
}1.3 TestExecutionListener
TestExecutionListener 定义了 6 个生命周期方法,默认均为空实现(模板方法模式):
public interface TestExecutionListener {
default void beforeTestClass(TestContext testContext) throws Exception {}
default void prepareTestInstance(TestContext testContext) throws Exception {}
default void beforeTestMethod(TestContext testContext) throws Exception {}
default void afterTestMethod(TestContext testContext) throws Exception {}
default void afterTestClass(TestContext testContext) throws Exception {}
}生命周期顺序:
beforeTestClass (一次)
└─ prepareTestInstance (一次)
└─ beforeTestMethod (每个测试方法)
└─ 实际测试方法执行
└─ afterTestMethod (每个测试方法)
└─ afterTestClass (一次)1.4 ContextCache
ContextCache 使用 MergedContextConfiguration 作为 key,ApplicationContext 作为 value。Key 的 equals/hashCode 基于以下因素:
- 测试类
- 配置类 / XML 配置文件
- Active Profiles
- 属性源位置
- ContextCustomizer
- Web 环境类型
public class DefaultContextCache implements ContextCache {
private final Map<MergedContextConfiguration, ApplicationContext> contextMap =
new LinkedHashMap<>(64, 0.75f, true);
private int maxSize = 32; // 默认最大缓存 32 个上下文
@Override
public ApplicationContext get(MergedContextConfiguration key) {
return this.contextMap.get(key);
}
@Override
public void put(MergedContextConfiguration key, ApplicationContext context) {
// 淘汰最久未访问的条目
if (this.contextMap.size() >= this.maxSize) {
evictLeastRecentlyUsed();
}
this.contextMap.put(key, context);
}
}性能提示:当多个测试类使用相同的配置时,ApplicationContext 会被共享复用,显著减少启动时间。
2. @ContextConfiguration 注解
@ContextConfiguration 是 TestContext 框架中最基础的注解,用于声明如何加载 ApplicationContext。
2.1 基本用法
@ContextConfiguration(classes = AppConfig.class)
public class UserServiceTest {
// 测试方法
}2.2 完整属性
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface ContextConfiguration {
// 指定配置类(替代 XML)
@AliasFor("locations")
Class<?>[] classes() default {};
// 指定 XML 配置文件路径
@AliasFor("classes")
String[] locations() default {};
// 指定 ContextInitializer 类
Class<? extends ContextInitializer>[] initializers() default {};
// 是否从父类继承配置(默认 true)
boolean inheritLocations() default true;
// 加载器,默认 AnnotationConfigContextLoader
Class<? extends ContextLoader> loader() default AnnotationConfigContextLoader.class;
// 指定配置类的名称(用于区分不同配置组合)
String name() default "";
}2.3 多种加载方式
基于注解配置类(推荐):
@ContextConfiguration(classes = {
DataSourceConfig.class,
ServiceConfig.class,
WebConfig.class
})
public class OrderServiceTest { }基于 XML:
@ContextConfiguration(locations = {
"classpath:spring/application-context.xml",
"classpath:spring/datasource-config.xml"
})
public class XmlBasedTest { }混合加载:
@ContextConfiguration(
classes = AppConfig.class,
locations = "classpath:spring/extra-beans.xml"
)
public class MixedConfigTest { }2.4 ContextInitializer
当需要在 ApplicationContext 刷新前注入自定义配置(如环境变量、系统属性)时,使用 ContextInitializer:
public class TestEnvironmentInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext ctx) {
// 添加测试专用的 PropertySource
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(
ctx, "payment.mock.gateway=http://localhost:9999"
);
}
}
// 在测试类上引用
@ContextConfiguration(
classes = AppConfig.class,
initializers = TestEnvironmentInitializer.class
)
public class PaymentTest { }2.5 配置继承与覆盖
// 基础测试抽象类
@ContextConfiguration(classes = BaseConfig.class)
public abstract class BaseTest { }
// 子类继承 BaseConfig,并追加 ExtraConfig
@ContextConfiguration(classes = ExtraConfig.class, inheritLocations = true)
public class ExtendedTest extends BaseTest {
// 最终的配置 = BaseConfig + ExtraConfig
}3. @SpringBootTest 与 @ContextConfiguration 的关系
@SpringBootTest 是 Spring Boot 在 @ContextConfiguration 基础上的封装,其本质是一个被 @ContextConfiguration 元标注的组合注解。
3.1 @SpringBootTest 的定义
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
public @interface SpringBootTest {
// 指定配置类,默认为 @SpringBootApplication 标注的类
@AliasFor("classes")
Class<?>[] value() default {};
@AliasFor("value")
Class<?>[] classes() default {};
// Web 环境类型
WebEnvironment webEnvironment() default WebEnvironment.MOCK;
// 启动参数
String[] args() default {};
enum WebEnvironment {
MOCK, // Mock Servlet 环境(内嵌 Tomcat 不启动)
RANDOM_PORT, // 随机端口启动内嵌容器
DEFINED_PORT, // 使用指定端口
NONE // 不创建 Web 环境
}
}3.2 SpringBootTestContextBootstrapper
关键区别在于 @SpringBootTest 指定了 @BootstrapWith(SpringBootTestContextBootstrapper.class),这是一个 TestContextBootstrapper 实现,负责:
- 自动查找主配置类:通过
@SpringBootApplication搜索入口类 - 自动启用 Auto-Configuration:使切片测试的自动配置生效
- 处理 Web 环境:根据
webEnvironment设置WebApplicationType
class SpringBootTestContextBootstrapper extends AbstractTestContextBootstrapper {
@Override
protected MergedContextConfiguration buildMergedContextConfiguration(
Class<?> testClass, ContextConfigurationOptions options) {
// 搜索 @SpringBootConfiguration
Class<?> configClass = SpringBootConfigurationFinder.find(testClass);
// 创建 MergedContextConfiguration
return new MergedContextConfiguration(testClass,
new Class<?>[]{configClass},
new String[]{}, // locations
new Class<?>[]{SpringBootConfigurationLoader.class},
this.activeProfiles,
this.propertySourceLocations,
this.contextCustomizers,
this.contextLoader
);
}
}3.3 对比总结
| 特性 | @ContextConfiguration | @SpringBootTest |
|---|---|---|
| 配置类声明 | 必须指定 | 自动查找 @SpringBootApplication |
| 自动配置 | ❌ 不启用 | ✅ 启用 |
| Web 环境 | 手动配置 | 内置支持 MOCK/RANDOM_PORT |
| 属性源 | 需 @TestPropertySource | 自动加载 application.properties |
| 切片测试 | ❌ 不支持 | ✅ 支持 |
| 上下文缓存 | 按配置类组合 | 按配置类组合 + Web 环境类型 |
使用原则:纯单元测试或简单集成测试用 @ContextConfiguration;涉及 Spring Boot 自动配置、切片测试或完整集成测试用 @SpringBootTest。
4. 切片测试原理
切片测试(Slice Test)是 Spring Boot 引入的一种轻量级集成测试方式,只加载被测功能所需的最小 Bean 集合,大幅提升测试速度。
4.1 常用切片注解
| 注解 | 加载的自动配置 |
|---|---|
@WebMvcTest | Web MVC 层(Controller、Jackson、Validator) |
@DataJpaTest | JPA 层(Entity、Repository、DataSource) |
@RestClientTest | RestTemplate/WebClient 相关 Bean |
@JsonTest | JSON 序列化/反序列化 |
@JdbcTest | JDBC 相关 Bean |
@DataMongoTest | MongoDB 相关 Bean |
@DataRedisTest | Redis 相关 Bean |
4.2 @WebMvcTest 示例
@WebMvcTest(controllers = UserController.class)
public class UserControllerSliceTest {
@Autowired
private MockMvc mockMvc;
@MockitoBean // Spring Boot 3.4+ 替代 @MockBean
private UserService userService;
@Test
void getUser_shouldReturnUser() throws Exception {
// 模拟 Service 层行为
given(userService.findById(1L))
.willReturn(new User(1L, "张三", "zhangsan@example.com"));
// 测试 Controller 层
mockMvc.perform(get("/api/users/1")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("张三"));
}
}4.3 切片测试原理 - TypeExcludeFilter
切片测试的核心机制是 TypeExcludeFilter,它从完整的自动配置列表中过滤掉不需要的配置:
// org.springframework.boot.test.autoconfigure.filter.TypeExcludeFilter
public class TypeExcludeFilter implements SmartInstantiationAwareBeanPostProcessor,
BeanFactoryAware {
private final Set<String> excludeClassNames = new HashSet<>();
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
// 仅注册符合切片范围的 BeanDefinition
for (String beanName : registry.getBeanDefinitionNames()) {
BeanDefinition bd = registry.getBeanDefinition(beanName);
if (!isIncluded(bd)) {
registry.removeBeanDefinition(beanName);
}
}
}
}每个切片注解通过 @ImportAutoConfiguration 引入自己需要的自动配置类。例如 @WebMvcTest:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
// 只导入 Web MVC 相关的自动配置
@AutoConfigureMockMvc
@AutoConfigureCache
@ImportAutoConfiguration({
DispatcherServletAutoConfiguration.class,
WebMvcAutoConfiguration.class,
JacksonAutoConfiguration.class,
// ... 仅 MVC 相关
})
public @interface WebMvcTest {
// ...
}4.4 @DataJpaTest 示例
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
public class UserRepositorySliceTest {
@Autowired
private UserRepository userRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void findByEmail_shouldReturnUser() {
// 通过 TestEntityManager 准备数据(不走 Service 层)
entityManager.persist(new User("李四", "lisi@example.com"));
// 测试 Repository 层
Optional<User> result = userRepository.findByEmail("lisi@example.com");
assertThat(result).isPresent();
assertThat(result.get().getName()).isEqualTo("李四");
}
}
@DataJpaTest默认使用内嵌内存数据库(如 H2),并且为每个测试方法开启事务并自动回滚。
5. 测试事务回滚
Spring TestContext 框架提供了一套完善的测试事务管理机制,默认行为是每个测试方法的事务自动回滚,确保测试之间相互隔离。
5.1 @Transactional 自动回滚原理
@SpringBootTest
@Transactional // 标注后,每个测试方法在事务内执行
public class TransactionalTest {
@Autowired
private UserRepository userRepository;
@Test
void testInsertUser() {
// 插入操作在事务内执行
userRepository.save(new User("王五", "wangwu@example.com"));
// 验证数据存在
assertThat(userRepository.findByEmail("wangwu@example.com")).isPresent();
// 方法结束后,事务自动回滚,数据不会真正写入数据库
}
}关键点:
@Transactional不标注在测试类上时,TransactionalTestExecutionListener不会开启事务- 标注后,每个
@Test方法在独立事务中执行 - 方法结束时,事务被回滚(而非提交)
5.2 @Commit 提交事务
@SpringBootTest
@Transactional
public class CommitTest {
@Autowired
private UserRepository userRepository;
@Test
@Commit // 覆盖默认回滚行为,提交事务
void testInsertAndCommit() {
userRepository.save(new User("赵六", "zhaoliu@example.com"));
// 此方法结束后数据会被实际写入数据库
}
}5.3 @Rollback 控制
@SpringBootTest
@Transactional
public class RollbackControlTest {
@Test
@Rollback(true) // 显式声明回滚(默认行为)
void shouldRollback() {
// 此方法回滚
}
@Test
@Rollback(false) // 不回滚
void shouldNotRollback() {
// 此方法不回滚
}
}5.4 事务传播行为
@SpringBootTest
@Transactional // 测试类级别事务
public class PropagationTest {
@Autowired
private OrderService orderService;
@Test
@Transactional(propagation = Propagation.REQUIRES_NEW)
void testRequiresNew() {
// 测试方法使用 REQUIRES_NEW,会挂起外部事务并创建新事务
// 但 TestContext 框架仍会在方法结束后回滚该新事务
}
}5.5 完整的事务生命周期
@BeforeEach
└─ TransactionalTestExecutionListener.beforeTestMethod()
└─ 开启事务(如果测试类/方法有 @Transactional)
└─ 测试方法执行
└─ TransactionalTestExecutionListener.afterTestMethod()
└─ 检查 @Rollback / @Commit
├─ 默认 → 回滚事务
├─ @Commit → 提交事务
└─ @Rollback(false) → 提交事务
└─ @AfterEach6. TestExecutionListener 接口及内置实现
6.1 SPI 加载机制
TestExecutionListener 通过 Spring 的 SpringFactoriesLoader 加载,配置文件位于:
META-INF/spring.factories内容示例(spring-test 模块):
org.springframework.test.context.TestExecutionListener=\
org.springframework.test.context.web.ServletTestExecutionListener,\
org.springframework.test.context.support.DependencyInjectionTestExecutionListener,\
org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener,\
org.springframework.test.context.event.ApplicationEventsTestExecutionListener,\
org.springframework.test.context.support.DirtiesContextTestExecutionListener,\
org.springframework.test.context.transaction.TransactionalTestExecutionListener,\
org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener,\
org.springframework.test.context.event.EventPublishingTestExecutionListenerSpring Boot 在此基础上追加:
# spring-boot-test-autoconfigure 模块
org.springframework.test.context.TestExecutionListener=\
org.springframework.boot.test.autoconfigure.SpringBootDependencyInjectionTestExecutionListener,\
org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener,\
org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener6.2 内置 Listener 详解
6.2.1 ServletTestExecutionListener
负责在测试方法执行前设置 MockHttpServletRequest、MockHttpServletResponse、MockHttpSession 等 Servlet API Mock 对象到 ApplicationContext 中。
public class ServletTestExecutionListener implements TestExecutionListener, Ordered {
@Override
public void beforeTestMethod(TestContext testContext) throws Exception {
if (isWebTest(testContext)) {
// 设置 Mock Servlet Context
setMockServletContext(testContext);
// 注入 HttpServletRequest 等代理
injectRequestAttributes(testContext);
}
}
}6.2.2 DependencyInjectionTestExecutionListener
实现依赖注入的核心,调用 AutowiredAnnotationBeanPostProcessor 等对测试实例进行依赖注入。
public class DependencyInjectionTestExecutionListener
extends AbstractTestExecutionListener {
@Override
public void prepareTestInstance(TestContext testContext) throws Exception {
injectDependencies(testContext);
}
protected void injectDependencies(TestContext testContext) throws Exception {
Object bean = testContext.getTestInstance();
ApplicationContext ac = testContext.getApplicationContext();
// 调用 ApplicationContext 的 AutowireCapableBeanFactory
AutowireCapableBeanFactory beanFactory = ac.getAutowireCapableBeanFactory();
beanFactory.autowireBeanProperties(bean,
AutowireCapableBeanFactory.AUTOWIRE_NO, false);
beanFactory.initializeBean(bean, testContext.getTestClass().getName());
}
}6.2.3 TransactionalTestExecutionListener
管理测试的事务生命周期。
public class TransactionalTestExecutionListener extends AbstractTestExecutionListener {
@Override
public void beforeTestMethod(TestContext testContext) throws Exception {
// 判断是否需要开启事务
if (testContext.getTestTransactionManager() != null
&& isTransactional(testContext)) {
TransactionStatus status = testContext.getTestTransactionManager()
.getTransaction(new DefaultTransactionDefinition());
testContext.setTransactionStatus(status);
}
}
@Override
public void afterTestMethod(TestContext testContext) throws Exception {
TransactionStatus status = testContext.getTransactionStatus();
if (status != null && !status.isCompleted()) {
// 根据 @Rollback 决定回滚还是提交
if (isRollback(testContext)) {
status.setRollbackOnly();
}
testContext.getTestTransactionManager().commit(status);
}
}
// 判断是否回滚(读取 @Rollback / @Commit)
private boolean isRollback(TestContext testContext) {
Rollback rollback = AnnotatedElementUtils
.findMergedAnnotation(testContext.getTestMethod(), Rollback.class);
if (rollback != null) {
return rollback.value();
}
Rollback classLevel = AnnotatedElementUtils
.findMergedAnnotation(testContext.getTestClass(), Rollback.class);
return classLevel != null ? classLevel.value() : true; // 默认回滚
}
}6.2.4 SqlScriptsTestExecutionListener
处理 @Sql 和 @SqlGroup 注解(详见第 8 节)。
6.2.5 DirtiesContextTestExecutionListener
当测试方法标注了 @DirtiesContext 时,在测试后清除并关闭 ApplicationContext。
6.2.6 ApplicationEventsTestExecutionListener
Spring Framework 5.3+ 引入,支持在测试中记录和断言 ApplicationEvent。
@SpringBootTest
public class EventTest {
@Autowired
private ApplicationEvents events;
@Autowired
private UserService userService;
@Test
void testUserRegistrationEmitsEvent() {
userService.register("test@example.com");
// 断言事件被发布
assertThat(events.stream(UserRegisteredEvent.class))
.hasSize(1)
.anyMatch(event -> event.getEmail().equals("test@example.com"));
}
}7. @MockBean / @SpyBean / @MockitoBean / @MockitoSpyBean 实现原理
7.1 概念演进
| 注解 | 引入版本 | 说明 |
|---|---|---|
@MockBean | Spring Boot 1.4 | 在 Spring 容器中创建 Mockito Mock |
@SpyBean | Spring Boot 1.4 | 在 Spring 容器中创建 Mockito Spy |
@MockitoBean | Spring Boot 3.4 | 替代 @MockBean,更清晰的命名 |
@MockitoSpyBean | Spring Boot 3.4 | 替代 @SpyBean |
7.2 使用示例
@SpringBootTest
public class OrderServiceTest {
// Mock 整个 PaymentGateway Bean
@MockitoBean
private PaymentGateway paymentGateway;
// Spy UserRepository,保留真实行为但可部分 stub
@MockitoSpyBean
private UserRepository userRepository;
@Autowired
private OrderService orderService;
@Test
void testCreateOrder() {
given(paymentGateway.charge(any()))
.willReturn(new PaymentResult("SUCCESS"));
Order order = orderService.createOrder(100L, BigDecimal.valueOf(99.99));
assertThat(order.getStatus()).isEqualTo(OrderStatus.PAID);
}
}7.3 实现原理 - MockitoPostProcessor
@MockBean 和 @SpyBean 的核心实现是 MockitoPostProcessor(Spring Boot 3.4 后更名为 MockitoBeanPostProcessor),它是一个 BeanDefinitionRegistryPostProcessor。
// org.springframework.boot.test.mock.mockito.MockitoPostProcessor
public class MockitoPostProcessor
implements BeanDefinitionRegistryPostProcessor, BeanPostProcessor {
private final Set<MockDefinition> mockDefinitions = new HashSet<>();
private final Set<SpyDefinition> spyDefinitions = new HashSet<>();
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
// 1. 扫描测试类上的 @MockBean 和 @SpyBean 注解
// 2. 将对应的 BeanDefinition 替换为 Mock/Spy 的 FactoryBean
for (MockDefinition def : mockDefinitions) {
BeanDefinition original = registry.getBeanDefinition(def.getBeanName());
// 使用 MockFactoryBean 替换原始 BeanDefinition
registry.registerBeanDefinition(def.getBeanName(),
new MockFactoryBeanDefinition(def, original));
}
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
// 3. 对于已经初始化的 Bean,使用 AOP 或直接替换
for (SpyDefinition def : spyDefinitions) {
if (def.matches(beanName)) {
return Mockito.spy(bean); // 包装为 Spy
}
}
return bean;
}
}7.4 注册流程
测试类实例化
└─ MockitoPostProcessor 扫描测试类上 @MockBean/@SpyBean
└─ 收集 MockDefinition / SpyDefinition
└─ postProcessBeanDefinitionRegistry:
├─ 为 @MockBean 创建 MockFactoryBean 并注册
└─ 为 @SpyBean 标记需要包装的 Bean
└─ postProcessAfterInitialization:
└─ 将原始 Bean 包装为 Mockito.spy()7.5 与普通 @Mock 的区别
| 特性 | @Mock (JUnit) | @MockBean / @MockitoBean |
|---|---|---|
| 作用范围 | 仅 JUnit 测试类字段 | 替换 Spring 容器中的 Bean |
| 自动注入 | 需手动 MockitoAnnotations.openMocks() | 自动注册到容器 |
| 影响范围 | 单测试类 | 影响整个 ApplicationContext 中所有注入点 |
| 集成测试 | 不适用 | 用于替换外部依赖 |
8. @Sql / @SqlGroup 注解
8.1 @Sql 基本用法
@SpringBootTest
public class SqlTest {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
@Sql("/sql/init-user.sql")
void testQueryUser() {
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM users", Integer.class);
assertThat(count).isEqualTo(3);
}
}/sql/init-user.sql 内容示例:
INSERT INTO users (id, name, email) VALUES (1, '张三', 'zhangsan@test.com');
INSERT INTO users (id, name, email) VALUES (2, '李四', 'lisi@test.com');
INSERT INTO users (id, name, email) VALUES (3, '王五', 'wangwu@test.com');8.2 阶段控制(executionPhase)
@SpringBootTest
public class SqlPhaseTest {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
@Sql(
scripts = "/sql/cleanup.sql",
executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD
)
@Sql(
scripts = "/sql/insert-test-data.sql",
executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD
)
@Sql(
scripts = "/sql/verify-cleanup.sql",
executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD
)
void testBusinessLogic() {
// 测试前已执行 cleanup.sql + insert-test-data.sql
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM orders", Integer.class);
assertThat(count).isGreaterThan(0);
// 测试后执行 verify-cleanup.sql
}
}8.3 @SqlGroup 组合
@Test
@SqlGroup({
@Sql("/sql/schema.sql"),
@Sql("/sql/init-data.sql"),
@Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
})
void testWithMultipleScripts() {
// 测试逻辑
}8.4 高级配置
@Test
@Sql(
scripts = "/sql/init-data.sql",
config = @SqlConfig(
dataSource = "secondaryDataSource", // 指定数据源
transactionManager = "secondaryTransactionManager", // 指定事务管理器
encoding = "UTF-8", // 编码
separator = "--", // SQL 分隔符
commentPrefixes = "##", // 注释前缀
errorMode = SqlConfig.ErrorMode.CONTINUE_ON_ERROR // 错误模式
)
)
void testWithCustomConfig() { }8.5 实现原理 - SqlScriptsTestExecutionListener
public class SqlScriptsTestExecutionListener extends AbstractTestExecutionListener {
@Override
public void beforeTestMethod(TestContext testContext) throws Exception {
executeSqlScripts(testContext, Sql.ExecutionPhase.BEFORE_TEST_METHOD);
}
@Override
public void afterTestMethod(TestContext testContext) throws Exception {
executeSqlScripts(testContext, Sql.ExecutionPhase.AFTER_TEST_METHOD);
}
private void executeSqlScripts(TestContext testContext,
Sql.ExecutionPhase phase) {
// 1. 从测试类和方法上收集 @Sql / @SqlGroup 注解
// 2. 筛选出指定 executionPhase 的脚本
// 3. 使用 ResourceDatabasePopulator 执行脚本
// 4. 如果存在 @Transactional,脚本在事务内执行
}
}ResourceDatabasePopulator 是 Spring JDBC 提供的 SQL 脚本执行工具:
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.addScript(new ClassPathResource("/sql/init-data.sql"));
populator.setContinueOnError(true);
populator.execute(dataSource);9. 源码分析:TestContextManager.beforeTestMethod() 完整流程
本节以 SpringExtension(JUnit 5 集成)为例,详细分析测试方法执行前的完整调用链。
9.1 整体调用链
JUnit5 执行 @Test 方法
└─ SpringExtension.beforeEach(ExtensionContext)
└─ TestContextManager.beforeTestMethod(testInstance, testMethod)
├─ 1. ServletTestExecutionListener.beforeTestMethod()
│ └─ 设置 MockHttpServletRequest / MockHttpServletResponse
├─ 2. DirtiesContextBeforeModesTestExecutionListener.beforeTestMethod()
│ └─ 如 @DirtiesContext(classMode=BEFORE_EACH_TEST_METHOD),清空缓存
├─ 3. ApplicationEventsTestExecutionListener.beforeTestMethod()
│ └─ 初始化 ApplicationEvents 记录器
├─ 4. MockitoTestExecutionListener.beforeTestMethod()
│ └─ 初始化 @Mock 注解字段
├─ 5. SpringBootDependencyInjectionTestExecutionListener.beforeTestMethod()
│ └─ 处理 @DynamicPropertySource 等Bootstrap属性
├─ 6. DependencyInjectionTestExecutionListener.beforeTestMethod()
│ └─ 再次确保依赖注入
├─ 7. TransactionalTestExecutionListener.beforeTestMethod()
│ └─ 检测 @Transactional → 开启事务
└─ 8. SqlScriptsTestExecutionListener.beforeTestMethod()
└─ 执行 BEFORE_TEST_METHOD 阶段的 SQL 脚本9.2 SpringExtension.beforeEach 源码
// org.springframework.test.context.junit.jupiter.SpringExtension
public class SpringExtension implements BeforeEachCallback, AfterEachCallback,
BeforeAllCallback, AfterAllCallback, TestInstancePostProcessor {
@Override
public void beforeEach(ExtensionContext context) throws Exception {
// 从 ExtensionContext 中获取测试实例和方法
Object testInstance = context.getRequiredTestInstance();
Method testMethod = context.getRequiredTestMethod();
// 获取或创建 TestContextManager(按测试类缓存)
TestContextManager manager = getTestContextManager(context);
// 调用 beforeTestMethod,触发所有 Listener
manager.beforeTestMethod(testInstance, testMethod);
}
private TestContextManager getTestContextManager(ExtensionContext context) {
Class<?> testClass = context.getRequiredTestClass();
// 从 ExtensionContext Store 中获取缓存的 TestContextManager
return context.getStore(NAMESPACE)
.getOrComputeIfAbsent(testClass, TestContextManager::new,
TestContextManager.class);
}
}9.3 TestContextManager.beforeTestMethod 源码
public class TestContextManager {
public void beforeTestMethod(Object testInstance, Method testMethod)
throws Exception {
// 设置当前测试实例和测试方法到 TestContext
this.testContext.updateTestInstance(testInstance);
this.testContext.updateTestMethod(testMethod);
this.testContext.updateTestException(null);
// 遍历所有 TestExecutionListener 并调用 beforeTestMethod
for (TestExecutionListener listener : this.testExecutionListeners) {
// 通过 Ordered / @Order 排序后的顺序执行
listener.beforeTestMethod(this.testContext);
}
}
}9.4 排序机制
TestExecutionListener 的排序通过 Ordered 接口实现:
// ServletTestExecutionListener 优先级最高(需要在其他 Listener 之前准备好 Servlet 环境)
public class ServletTestExecutionListener implements TestExecutionListener, Ordered {
// ORDER 定义在 Ordered.HIGHEST_PRECEDENCE + 1000
@Override
public int getOrder() { return HIGHEST_PRECEDENCE + 1000; }
}
// DependencyInjectionTestExecutionListener
public class DependencyInjectionTestExecutionListener
extends AbstractTestExecutionListener {
@Override
public int getOrder() { return 2000; }
}
// TransactionalTestExecutionListener
public class TransactionalTestExecutionListener
extends AbstractTestExecutionListener {
@Override
public int getOrder() { return 4000; }
}
// SqlScriptsTestExecutionListener 最后执行(确保事务已开启,SQL 在事务内执行)
public class SqlScriptsTestExecutionListener
extends AbstractTestExecutionListener {
@Override
public int getOrder() { return 5000; }
}9.5 TransactionalTestExecutionListener.beforeTestMethod 核心源码
public class TransactionalTestExecutionListener extends AbstractTestExecutionListener {
protected PlatformTransactionManager getTransactionManager(TestContext testContext) {
// 从 ApplicationContext 中获取事务管理器
// 优先使用 @Transactional 中指定的 transactionManager
ApplicationContext ctx = testContext.getApplicationContext();
return BeanFactoryAnnotationUtils.qualifiedBeanOfType(
ctx.getBeanFactory(), PlatformTransactionManager.class,
getTransactionManagerName(testContext));
}
@Override
public void beforeTestMethod(TestContext testContext) throws Exception {
// 1. 检查测试是否在事务上下文中
TestTransactionState state = testContext.getAttribute(
TRANSACTION_CONTEXT, TestTransactionState.class);
if (state != null && state.isTransactionActive()) {
return; // 已有事务,不重复开启
}
// 2. 检测 @Transactional 注解
boolean transactional = isTransactional(testContext);
if (!transactional) {
return;
}
// 3. 获取事务管理器
PlatformTransactionManager tm = getTransactionManager(testContext);
if (tm == null) {
throw new IllegalStateException("No transaction manager found");
}
// 4. 创建事务(默认使用 PROPAGATION_REQUIRED)
TransactionDefinition definition = createTransactionDefinition(testContext);
TransactionStatus status = tm.getTransaction(definition);
// 5. 保存事务状态,以便 afterTestMethod 回滚
testContext.setAttribute(TRANSACTION_CONTEXT,
new TestTransactionState(tm, status, definition));
}
private boolean isTransactional(TestContext testContext) {
// 检查方法级别 @Transactional
Method method = testContext.getTestMethod();
Transactional methodAnn = AnnotatedElementUtils
.findMergedAnnotation(method, Transactional.class);
if (methodAnn != null) {
return true;
}
// 检查类级别 @Transactional
Class<?> clazz = testContext.getTestClass();
Transactional classAnn = AnnotatedElementUtils
.findMergedAnnotation(clazz, Transactional.class);
return classAnn != null;
}
private TransactionDefinition createTransactionDefinition(
TestContext testContext) {
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// 从 @Transactional 读取传播行为、隔离级别、超时、只读标志
Transactional ann = AnnotatedElementUtils.findMergedAnnotation(
testContext.getTestMethod(), Transactional.class);
if (ann == null) {
ann = AnnotatedElementUtils.findMergedAnnotation(
testContext.getTestClass(), Transactional.class);
}
if (ann != null) {
def.setPropagationBehavior(ann.propagation().value());
def.setIsolationLevel(ann.isolation().value());
def.setTimeout(ann.timeout());
def.setReadOnly(ann.readOnly());
}
return def;
}
}9.6 完整的 afterTestMethod 流程
TestContextManager.afterTestMethod()
├─ 1. SqlScriptsTestExecutionListener.afterTestMethod()
│ └─ 执行 AFTER_TEST_METHOD 阶段的 SQL 脚本
├─ 2. TransactionalTestExecutionListener.afterTestMethod()
│ ├─ 如果 @Rollback (默认) → status.setRollbackOnly()
│ ├─ 如果 @Commit → 不设置 rollback-only
│ └─ tm.commit(status) → 实际上如果是 rollback-only 会触发回滚
├─ 3. ResetMocksTestExecutionListener.afterTestMethod()
│ └─ 重置所有 Mockito Mock 的状态
├─ 4. DirtiesContextTestExecutionListener.afterTestMethod()
│ └─ 如 @DirtiesContext → 关闭当前 ApplicationContext
└─ 5. EventPublishingTestExecutionListener.afterTestMethod()
└─ 发布测试执行事件10. 实战案例:支付订单全链路集成测试
本节构建一个完整的支付订单集成测试场景,包含控制器 → 服务 → 仓库三层,并 Mock 三方支付网关。
10.1 项目结构
com.example.payment/
├── controller/
│ └── PaymentController.java
├── service/
│ ├── PaymentService.java
│ └── PaymentGateway.java (接口)
├── service.impl/
│ ├── PaymentGatewayMockImpl.java (Mock 实现)
│ └── PaymentGatewayClient.java (实际三方客户端)
├── repository/
│ └── OrderRepository.java
├── entity/
│ └── OrderEntity.java
├── dto/
│ ├── CreateOrderRequest.java
│ └── PaymentResponse.java
└── config/
└── PaymentConfig.java10.2 实体与仓库
// OrderEntity.java
@Entity
@Table(name = "orders")
public class OrderEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private Long userId;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal amount;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private OrderStatus status;
@Column(nullable = false, unique = true)
private String orderNo;
@Column
private String paymentTransactionId;
@Column
private LocalDateTime paidAt;
@Column(nullable = false)
private LocalDateTime createdAt;
@PrePersist
void onCreate() {
this.createdAt = LocalDateTime.now();
this.status = OrderStatus.PENDING;
}
// getters / setters ...
}
// OrderStatus.java
public enum OrderStatus {
PENDING, // 待支付
PROCESSING, // 支付处理中
PAID, // 已支付
FAILED, // 支付失败
REFUNDED // 已退款
}
// OrderRepository.java
public interface OrderRepository extends JpaRepository<OrderEntity, Long> {
Optional<OrderEntity> findByOrderNo(String orderNo);
List<OrderEntity> findByUserIdAndStatus(Long userId, OrderStatus status);
}10.3 支付网关接口
// PaymentGateway.java
public interface PaymentGateway {
PaymentResult charge(ChargeRequest request);
PaymentResult refund(RefundRequest request);
}
// ChargeRequest.java
public class ChargeRequest {
private String orderNo;
private BigDecimal amount;
private String currency;
private String cardToken;
// getters / setters ...
}
// PaymentResult.java
public class PaymentResult {
private final String transactionId;
private final String status; // SUCCESS / FAILED
private final String errorCode;
private final String errorMessage;
public PaymentResult(String status) {
this(null, status, null, null);
}
public PaymentResult(String transactionId, String status,
String errorCode, String errorMessage) {
this.transactionId = transactionId;
this.status = status;
this.errorCode = errorCode;
this.errorMessage = errorMessage;
}
public boolean isSuccess() {
return "SUCCESS".equals(status);
}
// getters ...
}10.4 服务层
// PaymentService.java
@Service
public class PaymentService {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
public PaymentService(OrderRepository orderRepository,
PaymentGateway paymentGateway) {
this.orderRepository = orderRepository;
this.paymentGateway = paymentGateway;
}
@Transactional
public PaymentResponse createOrderAndPay(Long userId, CreateOrderRequest request) {
// 1. 创建订单
OrderEntity order = new OrderEntity();
order.setUserId(userId);
order.setAmount(request.getAmount());
order.setOrderNo(generateOrderNo());
order.setStatus(OrderStatus.PROCESSING);
orderRepository.save(order);
// 2. 调用三方支付网关
ChargeRequest chargeReq = new ChargeRequest();
chargeReq.setOrderNo(order.getOrderNo());
chargeReq.setAmount(order.getAmount());
chargeReq.setCurrency(request.getCurrency());
chargeReq.setCardToken(request.getCardToken());
PaymentResult paymentResult;
try {
paymentResult = paymentGateway.charge(chargeReq);
} catch (Exception e) {
order.setStatus(OrderStatus.FAILED);
orderRepository.save(order);
throw new PaymentException("支付网关调用失败", e);
}
// 3. 处理支付结果
if (paymentResult.isSuccess()) {
order.setStatus(OrderStatus.PAID);
order.setPaymentTransactionId(paymentResult.getTransactionId());
order.setPaidAt(LocalDateTime.now());
orderRepository.save(order);
return new PaymentResponse(order.getOrderNo(),
OrderStatus.PAID, paymentResult.getTransactionId());
} else {
order.setStatus(OrderStatus.FAILED);
orderRepository.save(order);
throw new PaymentException("支付失败: " + paymentResult.getErrorMessage());
}
}
@Transactional
public PaymentResponse refund(String orderNo) {
OrderEntity order = orderRepository.findByOrderNo(orderNo)
.orElseThrow(() -> new OrderNotFoundException(orderNo));
if (order.getStatus() != OrderStatus.PAID) {
throw new IllegalStateException("订单状态不允许退款: " + order.getStatus());
}
RefundRequest refundReq = new RefundRequest();
refundReq.setTransactionId(order.getPaymentTransactionId());
refundReq.setAmount(order.getAmount());
refundReq.setOrderNo(orderNo);
PaymentResult result = paymentGateway.refund(refundReq);
if (result.isSuccess()) {
order.setStatus(OrderStatus.REFUNDED);
orderRepository.save(order);
return new PaymentResponse(orderNo, OrderStatus.REFUNDED,
result.getTransactionId());
} else {
throw new PaymentException("退款失败: " + result.getErrorMessage());
}
}
private String generateOrderNo() {
return "ORD" + System.currentTimeMillis() +
String.format("%04d", ThreadLocalRandom.current().nextInt(10000));
}
}10.5 控制器层
// PaymentController.java
@RestController
@RequestMapping("/api/payments")
public class PaymentController {
private final PaymentService paymentService;
public PaymentController(PaymentService paymentService) {
this.paymentService = paymentService;
}
@PostMapping("/orders")
public ResponseEntity<PaymentResponse> createOrder(
@RequestBody CreateOrderRequest request,
@RequestHeader("X-User-Id") Long userId) {
PaymentResponse response = paymentService
.createOrderAndPay(userId, request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
@PostMapping("/orders/{orderNo}/refund")
public ResponseEntity<PaymentResponse> refund(
@PathVariable String orderNo) {
PaymentResponse response = paymentService.refund(orderNo);
return ResponseEntity.ok(response);
}
}