Writing Integration Tests for Spring Boot Web Applications: Spring Profiles

When we are writing integration tests for Spring Boot (web) applications, we typically don't want to use the configuration that's used when we run our application in the development, test, or production environment. This blog post describes how we can leverage Spring profiles when we have to create a separate configuration for our integration tests.

After we have finished this blog post, we:

  • Know how we can use different configuration in different environments.
  • Understand how we can build different application contexts for different environments.

Let's begin.

Using Different Configuration in Different Environments

It's quite common that we want to use different configuration and different configuration sources in different environments. For example, we might want to:

  • Use a different database connection configuration (JDBC url, username, and password) and different log level in different environments.
  • Use YAML configuration files (or properties files) in the development environment and environment variables in the test and production environments.

In the context of this blog post, we are mostly interested in using different configuration when we run our application (in the development environment) and when we run our integration tests. We can fulfill our goal by following these steps:

First, use these two Spring profiles:

  • The application profile must be active when we run our application in the local development environment.
  • The integrationTest profile must be active when we run our integration tests.

Second, create a new YAML configuration file called application.yml and put this file to the src/main/resources directory. This file contains the default configuration which can be overwritten by the profile specific configuration files.

Our application.yml file looks as follows:

logging:
  level:
    root: INFO
This configuration file doesn't have to contain all required configuration properties IF every profile specific configuration file provides values for the required configuration properties which aren't found from the application.yml file OR we provide the required configuration by using another configuration source (like environment variables).

Third, create the profile specific YAML configuration files and put these files to the src/main/resources directory. When we name these YAML configuration files, we must use this syntax: application-{spring profile name}.yml. Because we have two Spring profiles, we have to create two configuration files:

  • The application-application.yml file contains the configuration of the application Spring profile.
  • The application-integrationTest.yml file contains the configuration of the integrationTest Spring profile.

The application-application.yml file looks as follows:

spring:
  datasource:
    url: jdbc:postgresql:springmvc
    username: springmvc
    password: springmvc

The application-integrationTest.yml file looks as follows:

logging:
  level:
    root: DEBUG
spring:
  datasource:
    url: jdbc:tc:postgresql:16.1:///springmvctest
    username: springmvctest
    password: springmvctest

We have now created the YAML configuration files which allow us to use different configuration in different environments. The configuration that's used when we run our application (the active Spring profile is: application) looks as follows:

logging:
  level:
    root: INFO
spring:
  datasource:
    url: jdbc:postgresql:springmvc
    username: springmvc
    password: springmvc

The configuration that's used when we run our integration tests (the active Spring profile is: integrationTest) looks as follows:

logging:
  level:
    root: DEBUG
spring:
  datasource:
    url: jdbc:tc:postgresql:16.1:///springmvctest
    username: springmvctest
    password: springmvctest
There are three things I want to point out:

  • We can also put our configuration to properties files. If we want to use properties files, the name of the default configuration file must be application.properties and we must name the profile specific configuration files by using this syntax: application-{spring profile name}.properties.
  • We don't have to create a separate configuration file for the application profile. If we want to keep our configuration as simple as possible, we can put its configuration to the default configuration file.
  • We don't have to create multiple configuration files. It's also possible to split one physical configuration file into multiple logical files and add the profile specific configuration to these logical files (YAML example and properties file example).

Additional Reading:

Next, we will find out how we can build different application contexts for different environments.

Building Different Application Contexts for Different Environments

Sometimes we are in a situation where we want to build different application contexts for different environments. To be more specific, we might want to use different versions of the same Spring bean in different environments. For example, our application might have logic that depends on the current date and/or time and we want to write integration tests for that logic.

If we are in this situation, we must follow these steps:

First, create a constant class which specifies the Spring profiles used by our application. This class eliminates typos when we configure the Spring beans which are used in different environments and makes it easier to find the Spring beans which are used in a specific environment because we can simply use the 'Find Usages' function of our IDE. We can create this class by following these steps:

  1. Create a new class called Profiles and make sure that we cannot instantiate this class.
  2. Add two public and static constants to the created class. These constants specify the Spring profiles used by our example application (application and integrationTest).

After we have have created the Profiles class, its source code looks as follows:

public class Profiles {

    public static final String APPLICATION = "application";
    public static final String INTEGRATION_TEST = "integrationTest";

    private Profiles() {
    }
}

Second, create the application context configuration class which configures the Clock beans which are used by our application and our integration tests. We can create this class by following these steps:

  1. Create a new application context configuration class.
  2. Declare a new Clock bean and ensure that this bean is used when the active Spring profile is: application. Because this bean is used by our application, it must return the current instant.
  3. Declare a new Clock bean and ensure that this bean is used when the active Spring profile is: integrationTest. Because this bean is used by our integration tests, it must return a hard-coded instant.

After we have written our configuration class, its source code looks as follows:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;

@Configuration
public class DateTimeConfiguration {

    @Bean
    @Profile(Profiles.APPLICATION)
    public Clock systemTimeClock() {
        return Clock.systemDefaultZone();
    }

    @Bean
    @Profile(Profiles.INTEGRATION_TEST)
    public Clock fixedClock() {
        return Clock.fixed(Instant.parse("2024-04-12T16:00:00.00Z"),
                ZoneId.systemDefault()
        );
    }
}
We should be very careful when we use Spring profiles because:

  • The goal of our integration tests is to verify that our application is working as expected when it's deployed to the production environment. Every time when we replace a "real" bean with a test double, we increase the amount of production code that isn't covered by our integration tests. If the replaced code contains logic that must be tested, we have to either write other automated tests (such as end-to-end tests) or do more manual testing.
  • If we create too many Spring profiles and/or prefix the profile name with the NOT operator (!), we might create a situation where it's difficult to see which beans are loaded to the application context when a specific profile is active or isn't active.

We can now use different configuration in different environments and build different application contexts for different environments. Let's summarize what we learned from this blog post.

Summary

This blog post has taught us four things:

  • If we want to use different configuration in different environments, we can create profile specific configuration files (either YAML or properties files) or we can use a combination of profile specific configuration files and other configuration sources such as environment variables.
  • If we want to keep our configuration in one physical configuration file, we can split that configuration file into multiple logical files and add the profile specific configuration to these logical files.
  • If we want to declare Spring beans which are used in different environments (when a specific Spring profile is active), we have to annotate our application context configuration class or our bean definition method with the @Profile annotation.
  • We should be very careful when we use Spring profiles because we might create a situation where our integration tests cannot verify that our application is working when it's deployed to the production environment and/or it's difficult to see which beans are loaded to the application context when a specific profile is active or isn't active.
0 comments… add one

Leave a Reply