As a general best practice, we should externalize configuration values for our applications. This allows overriding them per stage and e.g. connect to a PayPal test instance for our development stage. Apart from overriding these properties for different stages, we can use the same technique when testing our applications to e.g. connect to a mocked external system. Spring Boot and Spring in general provide several mechanisms to override a configuration property for tests that we'll explore with this blog post.
Introduce a new property file for tests
The first approach for overriding properties helps whenever we have a set of static configuration values that are valid for multiple tests. We can create a default property file inside src/test/resources
and override common configuration values.
Let's take the following API endpoint as an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @RestController @RequestMapping("/") public class RootController { private final String welcomeMessage; public RootController(@Value("${welcome.message}") String welcomeMessage) { this.welcomeMessage = welcomeMessage; } @GetMapping public String getWelcomeMessage() { return welcomeMessage; } } |
This controller returns the value of the configuration property welcome.message
that is injected by Spring during runtime.
We can now override this property inside src/test/resources/application.properties
and define a value that is used for all tests that use the default profile.
1 | welcome.message=Test Default Profile Hello World! |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @WebMvcTest(RootController.class) class RootControllerTest { @Autowired private MockMvc mockMvc; @Test void shouldReturnDefaultWelcomeMessage() throws Exception { this.mockMvc .perform(MockMvcRequestBuilders.get("/")) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.content().string("Test Default Profile Hello World!")); } } |
Furthermore, we can also introduce new test specific profiles, e.g. test
, integration-test
, web-test
to group common configuration values. What's left is to activate the test profile with @ActiveProfiles
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @WebMvcTest(RootController.class) @ActiveProfiles("test") class RootControllerProfileTest { @Autowired private MockMvc mockMvc; @Test void shouldReturnDefaultWelcomeMessage() throws Exception { this.mockMvc .perform(MockMvcRequestBuilders.get("/")) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.content().string("Test Profile Hello World!")); } } |
The corresponding property filesrc/test/resources/application-test.properties
can then define the value:
1 | welcome.message=Test Profile Hello World! |
The same is true if we would use YAML-based property files (application.yml
or application-test.yml
).
We can even point to a custom property source that is e.g. not following the default Spring Boot conventions:
1 2 3 4 5 6 7 8 9 10 11 12 | @SpringBootTest @TestPropertySource("/custom.properties") class ApplicationPropertySourceTest { @Value("${welcome.message}") private String welcomeMessage; @Test void contextLoads() { assertEquals("Custom Property Source Hello World!", welcomeMessage); } } |
Use Spring Test support to override properties
Whenever we need to override a small set of Spring Boot configuration properties for a single test, introducing always a new profile is overkill. For such tests, overriding the values inline fits better.
All Spring Boot Test Slice annotations include the property
attribute. This allows adding properties to the Spring Environment
before running the test.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @WebMvcTest(value = RootController.class, properties = "welcome.message=Inline Hello World!") class RootControllerInlineTest { @Autowired private MockMvc mockMvc; @Test void shouldReturnDefaultWelcomeMessage() throws Exception { this.mockMvc .perform(MockMvcRequestBuilders.get("/")) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.content().string("Inline Hello World!")); } } |
Instead of specifying the properties as part of the Spring Boot test slice annotation, we can also use @TestPropertySource
here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @WebMvcTest(RootController.class) @TestPropertySource(properties = "welcome.message=Test Property Hello World!") class RootControllerTestPropertyTest { @Autowired private MockMvc mockMvc; @Test void shouldReturnDefaultWelcomeMessage() throws Exception { this.mockMvc .perform(MockMvcRequestBuilders.get("/")) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.content().string("Test Property Hello World!")); } } |
As mentioned, this also works for any other test slice annotation, like @SpringBootTest
:
1 2 3 4 5 6 7 8 9 10 11 | @SpringBootTest(properties = "welcome.message=Spring Boot Test Hello World!") class ApplicationTest { @Value("${welcome.message}") private String welcomeMessage; @Test void contextLoads() { assertEquals("Spring Boot Test Hello World!", welcomeMessage); } } |
ApplicationContextInitializer to dynamically override properties
Let's assume our application communicates to several external systems on startup. A good example might be initializing a Spring WebFlux WebClient
bean that fetches a valid JWT token from an authorization server on application startup.
We don't want our tests to depend on the uptime of this remote system and avoid any HTTP communication to external systems in general. An elegant solution for this is WireMock. With WireMock we can stub any HTTP response with a local webserver.
Talking to a local WireMock server for tests instead of the real authorization server requires overriding the base URL of our client. As we usually configure WireMock to use a random ephemeral port, we can't hard-code any URL for our test.
Hence we need a solution to dynamically override properties prior to starting the test application context. This is where the ApplicationContextInitializer
comes into play:
1 2 3 4 5 6 7 8 9 10 11 12 | public class WireMockInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { @Override public void initialize(ConfigurableApplicationContext applicationContext) { WireMockServer wireMockServer = new WireMockServer(new WireMockConfiguration().dynamicPort()); wireMockServer.start(); TestPropertyValues .of(Map.of("clients.order-api.base-url", wireMockServer.baseUrl() + "/orders")) .applyTo(applicationContext); } } |
Right after starting the WireMock server, we can apply a set of properties to our test application context using TestPropertyValues
as we know the URL of WireMock at this time.
To then make use of this custom initializer, we have to register it for our test.
1 2 3 4 5 6 7 8 9 10 11 12 13 | @SpringBootTest @ContextConfiguration(initializers = WireMockInitializer.class) class ApplicationInitializerTest { @Value("${clients.order-api.base-url}") private String orderApiBaseUrl; @Test void contextLoads() { System.out.println(orderApiBaseUrl); assertNotNull(orderApiBaseUrl); } } |
We make heavy use of this concept as a part of the Testing Spring Boot Applications Masterclass to stub several HTTP responses on application startup.
Override properties for unit tests
You might wonder: How can we override a Spring Boot property for our unit tests that don't create a Spring Test Context? That's easy!
Simply favor constructor injection, as this allows passing the value when instantiating your class under test. The following OrderService
injects a Set
of String
values to determine the shipping costs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | @Service public class OrderService { private final Set<String> freeShippingCountries; public OrderService(@Value("${order.free-shipping-countries}") Set<String> freeShippingCountries) { this.freeShippingCountries = freeShippingCountries; } public BigDecimal calculateShippingCosts(String countryCode) { if (freeShippingCountries.contains(countryCode)) { return BigDecimal.ZERO; } return new BigDecimal("4.99"); } } |
Overriding the injected order.free-shipping-countries
property for a unit test is now simple. We can pass the set of free shipping countries when instating the class under test using its public constructor:
1 2 3 4 5 6 7 8 9 10 11 | class OrderServiceTest { @Test void shouldReturnNoShippingCostsForFreeShippingCountries() { OrderService cut = new OrderService(Set.of("US")); BigDecimal result = cut.calculateShippingCosts("US"); assertEquals(BigDecimal.ZERO, result); } } |
No Spring Test support is needed here as this is a plain old unit test using only JUnit Jupiter.
However, if our class under test is using field injection (and we can't refactor for whatever reasons), there is a reflection-based solution available as a last resort. The Spring Test project provides a ReflectionTestUtils
class that we can use to set the private field via reflection.
Let's refactor the OrderService
in a bad way and use field injection:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @Service public class BadOrderService { @Value("${order.free-shipping-countries}") private Set<String> freeShippingCountries; public BigDecimal calculateShippingCosts(String countryCode) { if (freeShippingCountries.contains(countryCode)) { return BigDecimal.ZERO; } return new BigDecimal("4.99"); } } |
When instantiating the BadOrderService
we can only use the default constructor. Hence the freeShippingCountries
field is null right after instantiating the class under test.
We can now use the ReflectionTestUtils
to set the private field and inject the property:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class BadOrderServiceTest { @Test void avoidThis() { BadOrderService cut = new BadOrderService(); // you should avoid this and favor constructor injection ReflectionTestUtils.setField(cut, "freeShippingCountries", Set.of("US")); BigDecimal result = cut.calculateShippingCosts("US"); assertEquals(BigDecimal.ZERO, result); } } |
There are some obvious drawbacks to this approach as the test will fail whenever we change the name of the field. Use this tool with caution and rather favor constructor injection.
As a summary you can use the different approaches for the following use cases:
- create a dedicated test property file for common configuration values
- override properties inline if they are test specific (e.g. set a feature toggle to
false
) - use an
ApplicationContextInitializer
for dynamic properties - favor constructor injection for unit testing
The source code for this blog post on how to override properties for your Spring Boot tests is available on GitHub.
PS: For a more deep-dive when it comes to testing Spring Boot applications, consider enrolling for the Testing Spring Boot Applications Masterclass.
Have fun overriding your properties,
Phil