«1. Обзор

Хорошо известно, что автоконфигурация — одна из ключевых функций Spring Boot, но тестирование сценариев автоконфигурации может оказаться сложной задачей.

В следующих разделах мы покажем, как ApplicationContextRunner упрощает тестирование автоконфигурации.

2. Тестирование сценариев автоматической настройки

ApplicationContextRunner — это служебный класс, который запускает ApplicationContext и предоставляет утверждения в стиле AssertJ. Его лучше всего использовать как поле в тестовом классе для общей конфигурации, а затем мы вносим настройки в каждый тест:

private final ApplicationContextRunner contextRunner 
    = new ApplicationContextRunner();

Давайте перейдем к демонстрации его магии, протестировав несколько случаев.

2.1. Условие тестового класса

В этом разделе мы собираемся протестировать некоторые классы автоконфигурации, которые используют аннотации @ConditionalOnClass и @ConditionalOnMissingClass:

@Configuration
@ConditionalOnClass(ConditionalOnClassIntegrationTest.class)
protected static class ConditionalOnClassConfiguration {
    @Bean
    public String created() {
        return "This is created when ConditionalOnClassIntegrationTest "
               + "is present on the classpath";
    }
}

@Configuration
@ConditionalOnMissingClass(
    "com.baeldung.autoconfiguration.ConditionalOnClassIntegrationTest"
)
protected static class ConditionalOnMissingClassConfiguration {
    @Bean
    public String missed() {
        return "This is missed when ConditionalOnClassIntegrationTest "
               + "is present on the classpath";
    }
}

Мы хотели бы проверить, правильно ли автоконфигурация создает экземпляры или пропускает созданные и пропущенные компоненты с учетом ожидаемых условий.

ApplicationContextRunner предоставляет нам метод withUserConfiguration, в котором мы можем предоставить автоконфигурацию по требованию для настройки ApplicationContext для каждого теста.

Метод run принимает ContextConsumer в качестве параметра, который применяет утверждения к контексту. ApplicationContext будет закрыт автоматически при выходе из теста:

@Test
public void whenDependentClassIsPresent_thenBeanCreated() {
    this.contextRunner.withUserConfiguration(ConditionalOnClassConfiguration.class)
        .run(context -> {
            assertThat(context).hasBean("created");
            assertThat(context.getBean("created"))
              .isEqualTo("This is created when ConditionalOnClassIntegrationTest "
                         + "is present on the classpath");
        });
}

@Test
public void whenDependentClassIsPresent_thenBeanMissing() {
    this.contextRunner.withUserConfiguration(ConditionalOnMissingClassConfiguration.class)
        .run(context -> {
            assertThat(context).doesNotHaveBean("missed");
        });
}

В предыдущем примере мы видим простоту тестирования сценариев, в которых определенный класс присутствует в пути к классам. Но как мы собираемся проверить обратное, когда класс отсутствует в пути к классам?

Здесь в дело вступает FilteredClassLoader. Он используется для фильтрации определенных классов в пути к классам во время выполнения:

@Test
public void whenDependentClassIsNotPresent_thenBeanMissing() {
    this.contextRunner.withUserConfiguration(ConditionalOnClassConfiguration.class)
        .withClassLoader(new FilteredClassLoader(ConditionalOnClassIntegrationTest.class))
        .run((context) -> {
            assertThat(context).doesNotHaveBean("created");
            assertThat(context).doesNotHaveBean(ConditionalOnClassIntegrationTest.class);
        });
}

@Test
public void whenDependentClassIsNotPresent_thenBeanCreated() {
    this.contextRunner.withUserConfiguration(ConditionalOnMissingClassConfiguration.class)
        .withClassLoader(new FilteredClassLoader(ConditionalOnClassIntegrationTest.class))
        .run((context) -> {
            assertThat(context).hasBean("missed");
            assertThat(context).getBean("missed")
              .isEqualTo("This is missed when ConditionalOnClassIntegrationTest "
                         + "is present on the classpath");
            assertThat(context).doesNotHaveBean(ConditionalOnClassIntegrationTest.class);
        });
}

2.2. Test Bean Condition

Мы только что рассмотрели тестирование аннотаций @ConditionalOnClass и @ConditionalOnMissingClass, теперь давайте посмотрим, как все выглядит, когда мы используем аннотации @ConditionalOnBean и @ConditionalOnMissingBean.

Для начала нам также понадобятся несколько классов автоконфигурации:

@Configuration
protected static class BasicConfiguration {
    @Bean
    public String created() {
        return "This is always created";
    }
}
@Configuration
@ConditionalOnBean(name = "created")
protected static class ConditionalOnBeanConfiguration {
    @Bean
    public String createOnBean() {
        return "This is created when bean (name=created) is present";
    }
}
@Configuration
@ConditionalOnMissingBean(name = "created")
protected static class ConditionalOnMissingBeanConfiguration {
    @Bean
    public String createOnMissingBean() {
        return "This is created when bean (name=created) is missing";
    }
}

Затем мы вызовем метод withUserConfiguration, как в предыдущем разделе, и отправим наш пользовательский класс конфигурации, чтобы проверить, работает ли автоконфигурация. конфигурация соответствующим образом создает или пропускает bean-компоненты createOnBean или createOnMissingBean в различных условиях:

@Test
public void whenDependentBeanIsPresent_thenConditionalBeanCreated() {
    this.contextRunner.withUserConfiguration(
        BasicConfiguration.class, 
        ConditionalOnBeanConfiguration.class
    )
    // ommitted for brevity
}
@Test
public void whenDependentBeanIsNotPresent_thenConditionalMissingBeanCreated() {
    this.contextRunner.withUserConfiguration(ConditionalOnMissingBeanConfiguration.class)
    // ommitted for brevity
}

2.3. Тестирование условия свойства

В этом разделе давайте протестируем классы автоматической настройки, которые используют аннотации @ConditionalOnProperty.

Во-первых, нам нужно свойство для этого теста:

com.baeldung.service=custom

После этого мы пишем вложенные классы автоконфигурации для создания bean-компонентов на основе предыдущего свойства:

@Configuration
@TestPropertySource("classpath:ConditionalOnPropertyTest.properties")
protected static class SimpleServiceConfiguration {
    @Bean
    @ConditionalOnProperty(name = "com.baeldung.service", havingValue = "default")
    @ConditionalOnMissingBean
    public DefaultService defaultService() {
        return new DefaultService();
    }
    @Bean
    @ConditionalOnProperty(name = "com.baeldung.service", havingValue = "custom")
    @ConditionalOnMissingBean
    public CustomService customService() {
        return new CustomService();
    }
}

@Test
public void whenGivenCustomPropertyValue_thenCustomServiceCreated() {
    this.contextRunner.withPropertyValues("com.baeldung.service=custom")
        .withUserConfiguration(SimpleServiceConfiguration.class)
        .run(context -> {
            assertThat(context).hasBean("customService");
            SimpleService simpleService = context.getBean(CustomService.class);
            assertThat(simpleService.serve()).isEqualTo("Custom Service");
            assertThat(context).doesNotHaveBean("defaultService");
        });
}

@Test
public void whenGivenDefaultPropertyValue_thenDefaultServiceCreated() {
    this.contextRunner.withPropertyValues("com.baeldung.service=default")
        .withUserConfiguration(SimpleServiceConfiguration.class)
        .run(context -> {
            assertThat(context).hasBean("defaultService");
            SimpleService simpleService = context.getBean(DefaultService.class);
            assertThat(simpleService.serve()).isEqualTo("Default Service");
            assertThat(context).doesNotHaveBean("customService");
        });
}

Теперь мы вызываем метод withPropertyValues ​​для переопределения значения свойства в каждом тесте:

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

Подводя итог, в этом руководстве показано, как использовать ApplicationContextRunner для запуска ApplicationContext с настройками и применения утверждений.

Здесь мы рассмотрели наиболее часто используемые сценарии вместо исчерпывающего списка того, как настроить ApplicationContext.

Тем временем имейте в виду, что ApplicationConetxtRunner предназначен для не-веб-приложений, поэтому рассмотрите WebApplicationContextRunner для веб-приложений на основе сервлетов и ReactiveWebApplicationContextRunner для реактивных веб-приложений.