With this blog post, we’ll explore the various mechanisms for developers to override properties when testing their Spring Boot applications. We’ll take a look at both unit and integration tests and will use Java and JUnit Jupiter throughout the blog post. Furthermore, we discuss how too much property overriding can negatively impact our build time and best practices when it comes to reusing and grouping property definitions.

After working through this article, you’ll …

  • Know that there’s more than an application.properties file within src/test/resources to override properties
  • Learn how overriding properties works for Java unit and integration tests
  • Understand the Spring Boot configuration property hierarchy
  • Know the various static and dynamic property override mechanisms
  • Understand the downsides of specifying too many different configuration setups as this hinders the Spring TestContext framework from reusing the application context

Rationale Behind Overriding Properties and Design Decisions

Let’s start with the rationale behind overriding properties for our Spring Boot application.

First off, why should we allow someone to override our properties?

For a small proof of concept application, hardcoding configuration values like the JDBC URL for a DataSource or the maximum pool size of a thread pool may be a valid option. We’ll throw the PoC away, anyway, and never deploy it to production. Everything is started locally, and tweaking a configuration parameter is a matter of seconds.

However, feedback cycles for changing properties with direct source code changes become more difficult once our application is running on a remote server. When hardcoding the JDBC URL in a Java class, we need to at least recompile, repackage and re-deploy our application to connect to a different DataSource.

Things get worse if we have our application running on multiple environments. We would have to somehow maintain two versions of the same implementation, just with different hardcoded values.

Overall, hardcoding property values doesn’t seem like a viable approach.

Fortunately, there’s a solution for this available in any programming language: outsourcing our configuration values to a place outside the source code. This is even one part of the Twelve-Factor App: III. Config - Store Config in the Environment.

Next comes the question, what’s a configuration value that should be overridable?

Outsourcing every String or Integer value that represents a magic number as a configuration value is overkill. We should limit ourselves to values that we potentially want to override based on the environment or due to a business decision.

What are we talking about? Dynamic configuration properties, possibly simple feature toggles, rate limits, JDBC URLs, passwords, usernames, root URLs for our HTTP clients, etc.

The benefit of outsourcing these values lies in the faster change. We can deploy the same compiled and packaged Java application to development, staging, and production environments.

Depending on where we store the configuration values and how we obtain them (either once on startup or per use basis), we may even change the value during runtime without the need for redeployment.

On top of these benefits, being able to override our properties also helps us when testing our application. When writing integration tests, for example, we don’t want to connect to our production database - better to connect to a local test database.

Let’s explore how Spring Boot simplifies our lives as developers by making overriding properties a breeze.

Injecting and Resolving Configuration Properties with Spring Boot

Before we jump right into how we can override properties, we first have to understand how Spring Boot resolves properties.

Injecting configuration properties to a Java class with Spring Boot works in two ways. We can either use the @Value annotation or inject the Environment bean and query for a specific property:

@Component

public class TaxCalculator implements CommandLineRunner {

  // Option 1
  @Value("${custom.tax-rate}")
  private BigDecimal taxRate;

  // Option 2
  public TaxCalculator(Environment environment) {
    this.taxRate = environment.getProperty("custom.tax-rate", BigDecimal.class);
  }
}

As we can see from the example above, we’re not limited to injecting String, Boolean or numeric values. Spring Boot can serialize the properties to various Java classes.

Behind the scenes, Spring Boot will try to locate the configuration value for us. It does this at various places.

The most prominent source location is the application.properties (or its YAML variant application.yml) file within src/main/resources, e.g.:

custom.tax-rate=19.0

However, this is just one of the many property sources Spring Boot uses to resolve our property value.

There are over ten locations where Spring Boot searches for properties. Examples of this property hierarchy include: system properties, environment variables, command line arguments, and even more exotic (or nostalgic) places like JNDI attributes from java:comp/env.

To make matters worse, we can even write our own PropertyResolver to add further sources like a proprietary secret store or a data store.

These different property resolvers are put into a hierarchy. When Spring Boot resolves a property value and finds it at multiple places, the highest property resolver will determine the value.

Let’s use an example and assume we’re defining our tax rate in an environment variable as 7.0 and within application.properties as 19.0. In this example, our Spring Boot resolves 7.0 as the environmental property resolver has a higher order compared to our property file.

It’s important to keep the property order in mind when debugging issues with configuration values. The Spring team came up with this order to allow sensible property overrides.

Equipped with this theoretical knowledge, we can jump into our first testing use case: Overriding properties for unit tests.

Overriding Properties for Unit Tests

Before we dive into overriding properties for unit tests, let’s quickly define what we mean by a unit test in the context of this article. For the context of this article, we define a unit test as the following: A unit test verifies the behavior of a class in isolation while mocking services, repositories, and client components that the class under test depends on. Hence, we won’t work with an ApplicationContext for this test.

For demonstration purposes, we’re going to use the following ShipmentService class:

public class ShipmentService {

  private static final boolean FAST_SHIPMENT_ENABLED = true;

  public long calculateDuration(String shipmentLocale) {

    if (FAST_SHIPMENT_ENABLED) {
      return 3;
    } else {
      if (Set.of("DE", "GB", "FR").contains(shipmentLocale)) {
        return 5;
      } else {
        return 7;
      }
    }
  }
}

Right now, we’ve hardcoded the configuration value to toggle on fast shipments. This makes the configuration and testing efforts unnecessarily difficult. Based on the rationale in the last section, we’re refactoring the class to take the fast shipment feature flag as a configuration value.

Therefore, we adjust our class and inject the configuration value as part of the public constructor

public ShipmentServiceTwo(@Value("${fast-shipment-enabled}") boolean fastShipmentEnabled) {
  this.fastShipmentEnabled = fastShipmentEnabled;
}

The corresponding test can decide how to initialize the class under test and specify the configuration value(s) on a test basis:

@Test
void shouldReturnStaticDurationWhenFastShipmentIsEnabled() {
  ShipmentService cut = new ShipmentService(true);

  assertThat(cut.calculateDuration("US"))
          .isEqualTo(3);
}

When testing our class, we’re in full control of how our class under test is instantiated. This gives us the flexibility to pass the configuration on our own. We can even test different values by instantiating our class under test within our test.

In case we’re using field injection for our configuration values as the following:

@Value("{fast-shipment-enabled}")
private boolean fastShipmentEnabled;

And we can’t refactor our class design to get the values injected via a public constructor, we can fall back to reflection. Spring Test offers the ReflectionTestUtils class that we can use to inject the configuration value(s) after how class under test has been initiated:

@Test
void shouldReturnStaticDurationWhenFastShipmentIsEnabled() {
  ShipmentService cut = new ShipmentService();

  ReflectionTestUtils.setField(cut, "fastShipmentEnabled", true);

  assertThat(cut.calculateDuration("US"))
          .isEqualTo(3);
}

However, this solution is brittle as our unit test will fail if we rename our property and don’t consider this part when refactoring the application.

As a rule of thumb, we shouldn’t rely on any support and tools outside Mockito and JUnit when writing unit tests for our classes that use configuration properties. If possible, we should always aim for constructor injection and use the ReflectionTestUtils class only as a last resort.

If done right, there’s no need for any tooling from Spring Boot to test these classes. We can solely rely on our testing framework (e.g., JUnit Jupiter) and our mocking framework (e.g., Mockito).

Let’s move on and discuss overriding properties for integration tests.

Overriding Properties for Integration Tests

Things look different for integration tests. For our integration test we usually require more effort as we need to start infrastructure components. Booting our entire application, i.e. starting our embedded servlet container, and initializing all our beans usually won’t work in a vacuum. Our application won’t start if the required infrastructure components are not available or unreachable.

The perfect example is our database. Imagine we’re using Spring Data JPA and PostgreSQL as a relational database management system (RDBMS). Connecting to our production database for integration testing purposes has way too many downsides. We would intervene with the production workload, may leave the database with additional test data, and add computing credits to our monthly bill. Furthermore, providing network access to a production system from a developer’s machine may also require hacky and potentially insecure network workarounds.

That’s definitely something to avoid.

We’re better off provisioning our database on demand for integration testing purposes. Testcontainers comes in handy here. With Testcontainers, we can manage and orchestrate Docker containers for testing purposes.

Let’s use Testcontainers to provision our PostgreSQL database for testing purposes and see how we can override our configuration properties:

@Testcontainers
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class IntegrationTest {

  @Container
  static PostgreSQLContainer<?> postgreSQLContainer = new PostgreSQLContainer<>("postgres:12")
    .withPassword("inmemory")
    .withUsername("inmemory");

  @DynamicPropertySource
  static void postgresqlProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", postgreSQLContainer::getJdbcUrl);
    registry.add("spring.datasource.password", postgreSQLContainer::getPassword);
    registry.add("spring.datasource.username", postgreSQLContainer::getUsername);
  }

}

With the help of @DynamicPropertySource we can, as the name applies, dynamically override configuration properties during the launch of our ApplicationContext. As Testcontainers starts the database on a random ephemeral port, we need this dynamic nature as we can’t hardcode the port of the database because we simply don’t know it upfront.

If our integration test doesn’t have such a dynamic property override requirement, we can fall back to other property overriding techniques.

Imagine we want to write an integration test for a user journey while a feature toggle is activated. We can override this property per test basis with the help of @TestPropertySource:

@TestPropertySource(properties = "fast-shipment-enabled=true")
class IntegrationTest {

}

If we’re using a sliced context annotation like @WebMvcTest or @SpringBootTest to start our entire context, we can even define the property override within the annotation:

@SpringBootTest(properties = "fast-shipment-enabled=true")
class IntegrationTest {

}

Similar to @TestPropertySource, we can pass a list of key-value pairs separated by “=” as strings for the property values to override.

If we have similar test categories that all depend on a baseline configuration, copy-pasting the properties for each test would be too much boilerplate code. Instead, we can introduce a new Spring Boot profile and create a specific property file for it. Valid examples may be an integration test and a web-test profile.

When following the default Spring Boot naming convention and naming our profile file as application-{profile-name}.properties and placing the file inside src/test/resources, Spring Boot will pick it up for our tests:

custom.tax-rate=19.0

fast-shipment-enabled=false

We can then activate this specific testing profile for our tests with @ActiveProfiles("integration-test"), and they’ll share the same configuration baseline.

Considerations When Overriding Properties for Integration Tests

Having seen the various property override techniques, an important thing to keep in mind is that we shouldn’t wildly come up with many property overrides.

The reason behind this is that the Spring TestContext Framework caches our ApplicationContext for a potential later reuse. This makes our tests faster as a subsequent test can reuse a context and doesn’t have to start a fresh one. Starting a new one can take multiple seconds, depending on how much bootstrapping effort is made.

As we only work with an ApplicationContext for integration tests, this recommendation doesn’t apply to unit tests. When following the suggested approach of injecting the configuration value via the public constructor, we can safely change the configuration as we want.

For Spring to reuse the context, the context configuration has to match. Spring can’t reuse the context from IntegrationTestOne for IntegrationTestTwo if the latter declares a different JDBC URL or toggles a feature on instead of off.

If the context configuration matches Spring will reuse it, saving us valuable build times.

That’s why we should always try to consolidate similar context setups to make the most of this build time accelerator. Our tests shouldn’t all be using their own fine-tuned configuration, as this snowflake setup will result in no reuse.

Where possible, try to group the baseline configuration into a shared test profile and only override the properties carefully on a per tests basis where required.

Summary

Spring Boot offers various techniques for overriding properties. The techniques range from static property overrides with a new property file, to overriding the properties on a per test basis over more dynamic overrides using @DynamicPropertySource.

These techniques are quite helpful when tweaking the application configuration for our integration tests that interact with an ApplicationContext.

When it comes to unit testing our classes, we shouldn’t rely on any of these mechanisms. With a well-designed class, we should be able to pass the configuration values from the outside, preferably via the constructor, to test different scenarios.

When overriding configuration properties, we must keep in mind that each unique ApplicationContext setup requires Spring to start a new context for the test. This takes time and slows down our overall build time. If we share a similar context configuration between two test classes (aka. don’t change the configuration), Spring will reuse the ApplicationContext from the first test.