A unit test is used to verify the smallest part of an application (a “unit”) independent of other parts. This makes the verification process easy and fast since the scope of the testing is narrowed down to a class or method.
The @TestConfiguration
annotation is a useful aid for writing unit tests of components in a Spring Boot application. It allows us to define additional beans or override existing beans in the Spring application context to add specialized configurations for testing.
In this article, we will see the use of the @TestConfiguration
annotation for writing unit tests for a Spring Boot applications.
Example Code
This article is accompanied by a working code example on GitHub.Introducing the @TestConfiguration
Annotation
We use @TestConfiguration
to modify Spring’s application context during test runtime. We can use it to override certain bean definitions, for example to replace real beans with fake beans or to change the configuration of a bean to make it better testable.
We can best understand the @TestConfiguration
annotation by first looking at the @Configuration
annotation which is the parent annotation it inherits from.
Before that, let us create a Spring Boot project with the help of the Spring Boot Initializr, and then open the project in our favorite IDE.
We have added a dependency on Spring WebFlux in this project since we will work around configuring a bean for WebClient
in different ways in the test environment for accessing REST APIs. WebClient
is a non-blocking, reactive client to perform HTTP requests.
We will use this project to create our service class and bean configurations and then write tests using the @TestConfiguration
annotation.
Configuring a Test with @Configuration
Let us look at the structure of a unit test in Spring Boot where we define the beans in a configuration class annotated with the @Configuration
annotation:
@Configuration
public class WebClientConfiguration {
...
@Bean
public WebClient getWebClient
(final WebClient.Builder builder,
@Value("${data.service.endpoint}") String url) {
WebClient webClient = builder.baseUrl(url)
.defaultHeader(
HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_JSON_VALUE)
// more configurations and customizations
...
.build();
LOGGER.info("WebClient Bean Instance: {}", webClient);
return webClient;
}
}
In this code snippet, we configure the WebClient
bean to run requests against an external URL. We will next define a service class where we will inject this WebClient
bean to call a REST API:
@Service
public class DataService {
...
private final WebClient webClient;
public DataService(final WebClient webClient) {
this.webClient = webClient;
LOGGER.info("WebClient instance {}", this.webClient);
}
}
In this code snippet, the WebClient
bean is injected into the DataService
class. During testing, a WebClient
instance configured to use a different URL will be injected rather than the actual WebClient
bean.
We will now create our test class and annotate it with SpringBootTest
. This results in bootstrapping of the full application context containing the beans selected by component scanning. Due to this, we can inject any bean from the application context by autowiring the bean into our test class:
@SpringBootTest
@TestPropertySource(locations="classpath:test.properties")
class TestConfigurationExampleAppTests {
@Autowired
private DataService dataService;
...
}
In this code snippet, the DataService
bean injected in the test class uses the WebClient
bean configured with an external URL, which is defined in the property data.service.endpoint
located in the properties file test.properties
. This makes our unit test dependent on an external dependency, because the WebClient
is accessing a remote URL. This might fail if we run our test as part of any automated test or in any other environment with restricted connectivity.
Configuring a Test with @TestConfiguration
To make our unit tests run without any dependency on an external configuration, we may want to use a modified test configuration that will connect to a locally running mock service instead of bootstrapping the actual application context.
We do this by using the @TestConfiguration
annotation over our configuration class being used for the test. This test configuration class can be an inner class within a test class or a separate class as shown here:
@TestConfiguration
public class WebClientTestConfiguration {
...
@Bean
public WebClient getWebClient(final WebClient.Builder builder) {
//customized for running unit tests
WebClient webClient = builder
.baseUrl("http://localhost") // <-- local URL
.build();
...
...
return webClient;
}
}
@SpringBootTest
@Import(WebClientTestConfiguration.class)
@TestPropertySource(locations="classpath:test.properties")
class TestConfigurationExampleAppTests {
@Autowired
private DataService dataService;
...
}
Here the DataService
bean is injected in the test class and uses the WebClient
bean configured in the test configuration class with @TestConfiguration
annotation with local URL. This way we can execute our unit test without any dependency on an external system.
We are also overriding the behavior of the WebClient
bean to point to localhost
so that we can use a local instance of the REST API only for unit testing.
The @TestConfiguration
annotation provides the capability for defining additional beans or for modifying the behavior of existing beans in the Spring Application Context for applying customizations primarily required for running a unit test.
Enabling the Bean Overriding Behavior
Every bean in the Spring application context will have one or more unique identifiers. Bean overriding is registering or defining another bean with the same identifier as a result of which the previous bean definition is overridden with a new bean implementation.
The bean overriding feature is disabled by default from Spring Boot 2.1. A BeanDefinitionOverrideException is thrown if we attempt to override one or more beans.
We should not enable this feature during application runtime. However, we need to enable this feature during testing if we want to override one or more bean definitions.
We enable this feature by enabling the application property spring.main.allow-bean-definition-overriding
in a resource file as shown here:
spring.main.allow-bean-definition-overriding=true
Here we are setting the application property spring.main.allow-bean-definition-overriding
to true
in our resource file:test.properties
under test to enable bean overriding feature during testing.
Component Scanning Behavior
Though the @TestConfiguration
annotation inherits from the @Configuration
annotation, the main difference is that @TestConfiguration
is excluded during Spring Boot’s component scanning.
Configuration classes annotated with @TestConfiguration
are excluded from component scanning, so we need to import them explicitly in every test where we want to autowire them.
The @TestConfiguration
annotation is also annotated with the @TestComponent annotation in its definition to indicate that this annotation should only be used for testing.
Using @TestConfiguration
in Unit Tests
As explained earlier, we can use the @TestConfiguration
annotation in two ways during testing:
- Import test configuration using the
Import
annotation - Declaring
@TestConfiguration
as a static inner class
Using @TestConfiguration
with the @Import
Annotation
The Import
annotation is a class-level annotation that allows us to import the bean definitions from multiple classes annotated with the @Configuration
annotation or @TestConfiguration
annotation into the application context or Spring test context:
@TestConfiguration
public class WebClientTestConfiguration {
...
@Bean
public WebClient getWebClient(final WebClient.Builder builder) {
//customized for running unit tests
WebClient webClient = builder
.baseUrl("http://localhost")
.build();
...
...
return webClient;
}
}
@SpringBootTest
@Import(WebClientTestConfiguration.class)
class TestConfigurationExampleAppTests {
// Test case implementations
}
In this code snippet, our test configuration is defined in a separate class WebClientTestConfiguration
which is annotated with the @TestConfiguration
annotation. We then use the Import
annotation in our test class TestConfigurationExampleAppTests
to import this test configuration.
We should use the autowired injection to access the bean definitions declared in imported @TestConfiguration
classes.
Using @TestConfiguration
with a Static Inner Class
In this approach, the class annotated with @TestConfiguration
is implemented as a static inner class in the test class itself:
The Spring Boot test context will automatically discover it and load the test configuration if it is declared as a static inner class:
@SpringBootTest
public class UsingStaticInnerTestConfiguration {
@TestConfiguration
public static class WebClientConfiguration {
@Bean
public WebClient getWebClient(final WebClient.Builder builder) {
return builder.baseUrl("http://localhost").build();
}
}
@Autowired
private DataService dataService;
// Test methods of dataService
}
The test configuration is defined as a static inner class in this test. Here we do not need to import the test configuration explicitly.
Conclusion
In this post, we looked at how we can use the @TestConfiguration
annotation for creating a custom bean or for overriding an existing bean for unit testing of Spring applications.
Although we have talked of unit tests here, we can also use @TestConfiguration
in integration tests to add specialized bean configurations required for component interactions in specific test environments.
Here is a summary of the things we covered:
@TestConfiguration
annotation allows us to define additional beans or override existing beans in the Spring application context to add specialized configuration for testing.- We can use the
@TestConfiguration
annotation in two ways during testing:
- Declare the configuration in a separate class and then import the configuration in the test class
- Declare the configuration in a static inner class inside the test class
- The bean overriding feature is disabled by default. We enable this feature by switching on an application property
spring.main.allow-bean-definition-overriding
in our test.