«1. Обзор

В этом руководстве мы покажем, как мы можем предотвратить запуск bean-компонентов типа ApplicationRunner или CommandLineRunner во время интеграционных тестов Spring Boot.

2. Пример приложения

Наш пример приложения состоит из средства запуска командной строки, средства запуска приложения и bean-компонента службы задач.

Средство запуска командной строки вызывает метод execute службы задач, чтобы выполнить задачу при запуске приложения:

@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");
    }
}

Таким же образом средство запуска приложения взаимодействует со службой задач для выполнения другой задачи:

@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);
    }
}

И у нас также есть класс приложения Spring Boot, который заставляет все это работать:

@SpringBootApplication
public class ApplicationCommandLineRunnerApp {
    public static void main(String[] args) {
        SpringApplication.run(ApplicationCommandLineRunnerApp.class, args);
    }
}

3 Тестирование ожидаемого поведения

ApplicationRunnerTaskExecutor и CommandLineTaskExecutor запускаются после того, как Spring Boot загружает контекст приложения.

Мы можем проверить это с помощью простого теста:

@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 для применения шпионов Mockito к компонентам ApplicationRunnerTaskExecutor и CommandLineTaskExecutor. Таким образом мы можем убедиться, что метод run каждого из этих bean-компонентов был вызван один раз.

В следующих разделах мы рассмотрим различные способы и методы предотвращения этого поведения по умолчанию во время наших интеграционных тестов Spring Boot.

4. Предотвращение с помощью профилей Spring

Один из способов, которым мы можем предотвратить запуск этих двух, — это аннотировать их с помощью @Profile:

@Profile("!test")
@Component
public class CommandLineTaskExecutor implements CommandLineRunner {
    // same as before
}
@Profile("!test")
@Component
public class ApplicationRunnerTaskExecutor implements ApplicationRunner {
    // same as before
}

@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");
    }
}

После вышеуказанных изменений мы приступаем к нашему интеграционному тесту. :

Как мы видим, мы аннотировали приведенный выше тестовый класс аннотацией @ActiveProfiles(“test”), что означает, что он не будет связывать классы, аннотированные с помощью @Profile(“!test”). В результате ни компонент CommandLineTaskExecutor, ни компонент ApplicationRunnerTaskExecutor вообще не загружаются.

5. Предотвращение с помощью аннотации ConditionalOnProperty

@ConditionalOnProperty(
  prefix = "application.runner", 
  value = "enabled", 
  havingValue = "true", 
  matchIfMissing = true)
@Component
public class ApplicationRunnerTaskExecutor implements ApplicationRunner {
    // same as before
}
@ConditionalOnProperty(
  prefix = "command.line.runner", 
  value = "enabled", 
  havingValue = "true", 
  matchIfMissing = true)
@Component
public class CommandLineTaskExecutor implements CommandLineRunner {
    // same as before
}

Или мы можем настроить их связывание по свойству, а затем использовать аннотацию ConditionalOnProperty:

Как мы видим, ApplicationRunnerTaskExecutor и CommandLineTaskExecutor включены по умолчанию, и мы можем отключить их, если установим следующие свойства в false:

@SpringBootTest(properties = { 
  "command.line.runner.enabled=false", 
  "application.runner.enabled=false" })
class RunApplicationWithTestPropertiesIntegrationTest {
    // same as before
}

command.line.runner.enabled application.runner.enabled

Итак, в нашем тесте мы установили эти свойства в false и ни ни bean-компоненты CommandLineTaskExecutor не загружаются в контекст приложения:

Теперь, несмотря на то, что описанные выше методы помогают нам достичь нашей цели, есть случаи, когда мы хотим проверить, что все bean-компоненты Spring загружены и подключены правильно.

Например, мы можем захотеть проверить правильность внедрения bean-компонента TaskService в CommandLineTaskExecutor, но мы по-прежнему не хотим, чтобы его метод run выполнялся во время нашего теста. Итак, давайте посмотрим последний раздел, в котором объясняется, как мы можем этого добиться.

6. Предотвращение путем отказа от начальной загрузки всего контейнера

@BootstrapWith(SpringBootTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)

Здесь мы опишем, как мы можем предотвратить выполнение bean-компонентов CommandLineTaskExecutor и ApplicationRunnerTaskExecutor, не загружая весь контейнер приложения.

В предыдущих разделах мы использовали аннотацию @SpringBootTest, и это привело к загрузке всего контейнера во время наших интеграционных тестов. @SpringBootTest включает две мета-аннотации, относящиеся к этому последнему решению:

@ContextConfiguration(classes = {ApplicationCommandLineRunnerApp.class},
  initializers = ConfigDataApplicationContextInitializer.class)

Хорошо, если нет необходимости загружать весь контейнер во время нашего теста, тогда не нужно использовать @BootstrapWith.

Вместо этого мы можем заменить его на @ContextConfiguration:

С помощью @ContextConfiguration мы определяем, как загружать и настраивать контекст приложения для интеграционных тестов. Установив свойство классов ContextConfiguration, мы объявляем, что Spring Boot должен использовать класс ApplicationCommandLineRunnerApp для загрузки контекста приложения. Определив инициализатор как ConfigDataApplicationContextInitializer, приложение загружает его свойства.

@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());
    }
}

«Нам по-прежнему нужен @ExtendWith(SpringExtension.class), так как он интегрирует Spring TestContext Framework в модель программирования JUnit 5 Jupiter.

В результате вышеизложенного контекст приложения Spring Boot загружает компоненты и свойства приложения без выполнения компонентов CommandLineTaskExecutor или ApplicationRunnerTaskExecutor:

Кроме того, мы должны помнить, что ConfigDataApplicationContextInitializer, когда используется отдельно, не поддерживает внедрение @Value(“${…}†). Если мы хотим его поддерживать, нам нужно настроить PropertySourcesPlaceholderConfigurer.

7. Заключение