1. Overview
It’s well known that auto-configuration is one of the key features in Spring Boot, but testing auto-configuration scenarios can be tricky.
In the following sections, we’ll show how ApplicationContextRunner simplifies auto-configuration testing.
2. Test Auto-Configuration Scenarios
ApplicationContextRunner is a utility class which runs the ApplicationContext and provides AssertJ style assertions. It’s best used as a field in test class for shared configuration and we make customizations in each test afterward:
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner();
Let’s move on to show its magic by testing a few cases.
2.1. Test Class Condition
In this section, we’re going to test some auto-configuration classes which use @ConditionalOnClass and @ConditionalOnMissingClass annotations:
@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"; } }
We’d like to test whether the auto-configuration properly instantiates or skips the created and missed beans given expected conditions.
ApplicationContextRunner gives us the withUserConfiguration method where we can provide an auto-configuration on demand to customize the ApplicationContext for each test.
The run method takes a ContextConsumer as a parameter which applies the assertions to the context. The ApplicationContext will be closed automatically when the test exits:
@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"); }); }
Through the preceding example, we see the simpleness of testing the scenarios in which a certain class is present on the classpath. But how are we going to test the converse, when the class is absent on the classpath?
This is where FilteredClassLoader kicks in. It’s used to filter specified classes on the classpath at runtime:
@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
We’ve just looked at testing @ConditionalOnClass and @ConditionalOnMissingClass annotations, now let’s see what things look like when we are using @ConditionalOnBean and @ConditionalOnMissingBean annotations.
To make a start, we similarly need a few auto-configuration classes:
@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"; } }
Then, we’d call the withUserConfiguration method like the preceding section and send in our custom configuration class to test if the auto-configuration appropriately instantiates or skips createOnBean or createOnMissingBean beans in different conditions:
@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. Test Property Condition
In this section, let’s test the auto-configuration classes which use @ConditionalOnProperty annotations.
First, we need a property for this test:
com.baeldung.service=custom
After that, we write nested auto-configuration classes to create beans based on the preceding property:
@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(); } }
Now, we’re calling the withPropertyValues method to override the property value in each test:
@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"); }); }
3. Conclusion
To sum up, this tutorial just showed how to use ApplicationContextRunner to run the ApplicationContext with customizations and apply assertions.
We covered the most frequently used scenarios in here instead of an exhaustive list of how to customize the ApplicationContext.
In the meantime, please bear in mind that the ApplicationConetxtRunner is for non-web applications, so consider WebApplicationContextRunner for servlet-based web applications and ReactiveWebApplicationContextRunner for reactive web applications.
The source code for this tutorial can be found over on GitHub.