Cookies

This website uses cookies to obtain statistics from users navigation. If you go on browsing we consider you accept them.

Test containers with Spring Boot. Persistence layer integration testing

Nowadays, there is no doubt that the quality of software and its flexibility are directly linked to the ability to verify if it still works after a change, a refactor or a new functionality. Time and effort are dedicated developing automated tests for our applications in order to keep adapting to changes while making sure our software keeps doing what it was doing before.

A technology that can help us considerably when verifying that our software works as expected is Test containers. Test containers is an open-source framework that allows us to deploy infrastructure dependencies of our application when running tests, so that we can test the application against real infrastructure dependencies.

Test containers

We can deploy relational databases, non-relational databases, messaging brokers as well as any software deployable within a Docker container. We just need our tests to be able to communicate with a Docker daemon and deploy the test containers that we have configured in our test code.

¿What problems do Test containers solve?

By allowing us to automatically deploy infrastructure dependencies of our application, test containers makes it easier for us to write integration tests that make sure that the components of our application work correctly on the specific infrastructure. Without test containers, we would have to manage the instance of this infrastructure dependency externally. Using a shared external instance introduces problems and limitations when initializing schemes, fixture data, concurrent test executions by some developers, manual infrastructure deploy and so on.

With test containers, our tests always start with a new instance dedicated only for that test execution and we do not have to worry about all these situations.

Test containers in Spring Boot

Let’s put our hands on Test containers! In this article the purpose is going to be to test the persistence layer of a microservice. The microservice is a simple locations REST API, already used in other website articles such as Java 21 service with virtual threads performance test. The source code is available in the Github repository Spring Boot Location API. In order to start the service it is necessary to have Docker installed and a Java 21 JDK or higher.

The idea of this exercise is to prove that the application is able to use PostgreSQL and MongoDB as its persistence engine. Let’s start with PostgreSQL.

Adding the PostgreSQL infrastructure tests

ext {
    springBootVersion = '3.2.8'
    springDependencyManagementVersion = '1.1.4'
}

plugins {
    id 'java'
    id 'org.springframework.boot' version "${springBootVersion}"
    id 'io.spring.dependency-management' version "${springDependencyManagementVersion}"
}

dependencies {
    // Other dependencies...


    testImplementation 'org.springframework.boot:spring-boot-testcontainers'
    testImplementation 'org.testcontainers:postgresql'

}

Adding all these dependencies to our build.gradle file we are adding the module to use test containers in Spring Boot and also the test container module to be able to support PostgreSQL test containers. The next step is going to be configuring the docker image we need to use in our test container. In order to use it in our Spring Boot tests it is going to be necessary to initialize it as a Spring bean.

@TestConfiguration(proxyBeanMethods = false)
public class LocationApiTestContainersConfiguration {

    @Bean
    @ServiceConnection
    PostgreSQLContainer<?> postgresqlContainer() {

        return new PostgreSQLContainer<>(DockerImageName.parse("postgres:16-alpine"))
            .withUrlParam("binaryTransfer", "true")
            .withUrlParam("rewriteBatchedInserts", "true");
    }
// ...


}

With the @ServiceConnection annotation, Spring is going to configure the connection details against the deployed docker container. Now that our tests are able to start a PostgreSQL instance, it is already possible to write the infrastructure integration tests.

public abstract class PersistenceIntegrationTest {

    @Autowired
    LocationRepository locationRepository;

    @Test
    void testLocationDoesNotExist() {
        LocationId locationId = new LocationId(1);

        Assertions.assertThatThrownBy(() -> locationRepository.get(locationId))
                .isInstanceOf(EntityNotFoundException.class);
    }

    // ...

}

The design pattern Template method test pattern has been chosen in order to be able to reuse our tests for each of the supported persistence engines. The abstract class PersistenceIntegrationTest is going to be used as a test template some test classes implementations may extend.

@Import(LocationApiTestContainersConfiguration.class)
@ActiveProfiles("postgres")
@SpringBootTest(classes = {TestLocationApiApplication.class})
@Rollback
public class PostgresIntegrationTest extends PersistenceIntegrationTest {
}

The PostgresIntegrationTest inherits the PersistenceIntegrationTest test methods and enables the postgres Spring profile. This profile is going to be the one used in the application to activate the PostgreSQL persistence and it will also initialize the LocationRepository implementation responsible for persisting our domain objects into the PostgreSQL RDBMS.

Adding the MongoDB infrastructure tests

The first step is to add the required modules for the mongo test container.

dependencies {

    // Other dependencies...


    testImplementation 'org.testcontainers:mongodb'
}

And the required bean to start the mongo test container.

@TestConfiguration(proxyBeanMethods = false)
public class LocationApiTestContainersConfiguration {
    // ...


    @Profile("mongo")
    @Bean
    @ServiceConnection
    MongoDBContainer mongoContainer() {
        return new MongoDBContainer("mongo:6.0.16");
    }
}

As the last step, another inheritor class of the PersistenceIntegrationTest is created in order to execute the MongoDB infrastructure integration tests.

@Import(LocationApiTestContainersConfiguration.class)
@ActiveProfiles("mongo")
@SpringBootTest(classes = {TestLocationApiApplication.class})
public class MongoIntegrationTest extends PersistenceIntegrationTest {
}

At this point a project framework has been created in order to use a CI environment that guarantees in each build that our application still works against both database engines and also the exact docker image version of the database that we are using in our production environments.

Test containers and Github Actions

Closing the article, I wanted to remark the use of Github Actions as a simple and available CI engine. In the example repository it can be checked that Github Actions builds the artifact with Gradle, executing the tests with test containers as a build requirement.