1. 概述
在 Spring Boot 集成测试中,我们经常会遇到一个实际问题:ApplicationRunner
和 CommandLineRunner
类型的 Bean 会在应用上下文加载后自动执行。这在测试场景中往往是不希望发生的——比如我们只想验证 Bean 是否正确注入,而不是触发其业务逻辑。
本文将介绍几种简单粗暴但有效的方式,来阻止这两类 Runner 在测试期间执行,避免“踩坑”。
2. 示例应用
我们先来看一个典型的场景:应用启动时通过 CommandLineRunner
和 ApplicationRunner
触发一些任务。
✅ CommandLineRunner 实现
@Component
public class CommandLineTaskExecutor implements CommandLineRunner {
private TaskService taskService;
public CommandLineTaskExecutor(TaskService taskService) {
this.taskService = taskService;
}
@Override
public void run(String... args) throws Exception {
taskService.execute("command line runner task");
}
}
✅ ApplicationRunner 实现
@Component
public class ApplicationRunnerTaskExecutor implements ApplicationRunner {
private TaskService taskService;
public ApplicationRunnerTaskExecutor(TaskService taskService) {
this.taskService = taskService;
}
@Override
public void run(ApplicationArguments args) throws Exception {
taskService.execute("application runner task");
}
}
✅ 任务服务类
@Service
public class TaskService {
private static Logger logger = LoggerFactory.getLogger(TaskService.class);
public void execute(String task) {
logger.info("do " + task);
}
}
✅ 启动类
@SpringBootApplication
public class ApplicationCommandLineRunnerApp {
public static void main(String[] args) {
SpringApplication.run(ApplicationCommandLineRunnerApp.class, args);
}
}
以上结构非常常见,用于在应用启动时执行初始化任务,比如加载缓存、注册服务等。
3. 验证默认行为
⚠️ 默认情况下,只要 Spring Boot 上下文加载完成,这两个 Runner 就会自动执行。
我们可以通过一个简单的测试来验证这一点:
@SpringBootTest
class RunApplicationIntegrationTest {
@SpyBean
ApplicationRunnerTaskExecutor applicationRunnerTaskExecutor;
@SpyBean
CommandLineTaskExecutor commandLineTaskExecutor;
@Test
void whenContextLoads_thenRunnersRun() throws Exception {
verify(applicationRunnerTaskExecutor, times(1)).run(any());
verify(commandLineTaskExecutor, times(1)).run(any());
}
}
✅ 使用 @SpyBean
可以对已存在的 Bean 进行行为监控。
✅ 测试通过说明:两个 Runner 的 run()
方法确实各被执行了一次。
接下来我们看如何在测试中“关掉”它们。
4. 使用 Spring Profiles 控制加载
最直接的方式是通过 Profile 控制 Bean 的加载条件。
给两个 Runner 加上 @Profile("!test")
,表示“非 test 环境才加载”。
@Profile("!test")
@Component
public class CommandLineTaskExecutor implements CommandLineRunner {
// 原有逻辑不变
}
@Profile("!test")
@Component
public class ApplicationRunnerTaskExecutor implements ApplicationRunner {
// 原有逻辑不变
}
然后在测试类上激活 test
Profile:
@ActiveProfiles("test")
@SpringBootTest
class RunApplicationWithTestProfileIntegrationTest {
@Autowired
private ApplicationContext context;
@Test
void whenContextLoads_thenRunnersAreNotLoaded() {
assertNotNull(context.getBean(TaskService.class));
assertThrows(NoSuchBeanDefinitionException.class,
() -> context.getBean(CommandLineTaskExecutor.class),
"CommandLineRunner should not be loaded during this integration test");
assertThrows(NoSuchBeanDefinitionException.class,
() -> context.getBean(ApplicationRunnerTaskExecutor.class),
"ApplicationRunner should not be loaded during this integration test");
}
}
✅ 效果:Runner 类根本不会被注册到容器中。
⚠️ 缺点:如果测试需要验证这些 Bean 是否被正确注入(只是不想执行 run
),这种方式就太“暴力”了。
5. 使用 @ConditionalOnProperty 动态控制
更灵活的方式是通过配置项控制是否启用 Runner。
使用 @ConditionalOnProperty
注解:
@ConditionalOnProperty(
prefix = "application.runner",
value = "enabled",
havingValue = "true",
matchIfMissing = true)
@Component
public class ApplicationRunnerTaskExecutor implements ApplicationRunner {
// 原有逻辑
}
@ConditionalOnProperty(
prefix = "command.line.runner",
value = "enabled",
havingValue = "true",
matchIfMissing = true)
@Component
public class CommandLineTaskExecutor implements CommandLineRunner {
// 原有逻辑
}
✅ matchIfMissing = true
表示默认开启。
✅ 只需在测试中关闭即可:
@SpringBootTest(properties = {
"command.line.runner.enabled=false",
"application.runner.enabled=false" })
class RunApplicationWithTestPropertiesIntegrationTest {
// 测试逻辑
}
✅ 优点:灵活,可通过配置动态控制。
✅ 适合需要保留 Bean 定义但不想执行的场景。
6. 不启动完整容器:使用 @ContextConfiguration
如果你的测试 不需要启动整个 Spring Boot 应用上下文,而是只需要加载部分 Bean 并验证依赖关系,那么可以换一种思路:不走 SpringApplication 启动流程。
❌ 问题根源
@SpringBootTest
会触发完整的 SpringApplication 流程,包括 Runner 的执行。
它底层依赖:
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
✅ 解决方案:手动加载上下文
使用 @ContextConfiguration
替代 @SpringBootTest
:
@ExtendWith(SpringExtension.class)
@ContextConfiguration(
classes = { ApplicationCommandLineRunnerApp.class },
initializers = ConfigDataApplicationContextInitializer.class
)
public class LoadSpringContextIntegrationTest {
@SpyBean
TaskService taskService;
@SpyBean
CommandLineRunner commandLineRunner;
@SpyBean
ApplicationRunner applicationRunner;
@Test
void whenContextLoads_thenRunnersDoNotRun() throws Exception {
assertNotNull(taskService);
assertNotNull(commandLineRunner);
assertNotNull(applicationRunner);
verify(taskService, times(0)).execute(any());
verify(commandLineRunner, times(0)).run(any());
verify(applicationRunner, times(0)).run(any());
}
}
✅ 效果:
- 所有 Bean 都被加载和注入 ✅
- 但 Runner 的
run()
方法不会被执行 ❌
⚠️ 注意事项:
ConfigDataApplicationContextInitializer
不支持@Value("${...}")
注入。- 如需支持,需手动注册
PropertySourcesPlaceholderConfigurer
。
7. 总结
方法 | 适用场景 | 是否保留 Bean | 是否执行 run() |
---|---|---|---|
@Profile("!test") |
简单粗暴,测试环境彻底禁用 | ❌ 不加载 | ❌ 不执行 |
@ConditionalOnProperty |
需要灵活控制开关 | ✅ 加载 | ❌ 可控制不执行 |
@ContextConfiguration |
仅验证依赖注入,不启动完整应用 | ✅ 加载 | ❌ 不执行 |
✅ 推荐选择:
- 如果只是想“跳过执行”,推荐 **
@ConditionalOnProperty
**。 - 如果测试不依赖 SpringApplication 生命周期,推荐 **
@ContextConfiguration
**。
代码已上传至 GitHub:https://github.com/baomidou/tutorials/tree/master/spring-boot-testing