1. 概述

在 Spring Boot 集成测试中,我们经常会遇到一个实际问题:ApplicationRunnerCommandLineRunner 类型的 Bean 会在应用上下文加载后自动执行。这在测试场景中往往是不希望发生的——比如我们只想验证 Bean 是否正确注入,而不是触发其业务逻辑。

本文将介绍几种简单粗暴但有效的方式,来阻止这两类 Runner 在测试期间执行,避免“踩坑”。

2. 示例应用

我们先来看一个典型的场景:应用启动时通过 CommandLineRunnerApplicationRunner 触发一些任务。

✅ 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


原始标题:Prevent ApplicationRunner or CommandLineRunner Beans From Executing During Junit Testing