#HOWTO: Write Spring Boot integration tests with a ‘real’ database

Today I’ll show you a way to write integration tests for your Spring Boot based application with a ‘real’ database and not an embedded database like H2. In the last weeks, I was looking for a solution to write integration tests for my Spring Data based application which was using a Postgres database. I had the following requirements for this task:

  1. The integration tests should use the same database as in production (referring to the Twelve-Factor App I wanted to keep my environment during the tests as similar as possible to the production environment)
  2. The tests should not need any pre-setup before running (e.g. like manually setting up a test database)
  3. The tests should be able to be parallelized
  4. The tests should use my Flyway DDL scripts and create-drop (spring.jpa.hibernate.ddl-auto) shouldn’t be activated  for my tests
  5. Good integration with the excellent Spring tests environment

For this task, I found the awesome project: Testcontainers. The project describes itself as the following:

“Testcontainers is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container.”

For using this dependency you need to have Docker on your local machine/on your build server (Jenkins etc.). With this project, you can use a @ClassRule or @Rule on each of your integration tests and define the Docker image for your test. For MySQL and Postgres and there are already built-in solutions but you are free to use the image of your choice like the following:

// generic container for self-defined Docker images
@ClassRule
public static GenericContainer redis = new GenericContainer("redis:3.0.6").withExposedPorts(6379);

// built-in containers
@ClassRule
public static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer().withPassword("inmemory")
            .withUsername("inmemory");

To meet my third requirement (The tests should be able to be parallelized) I configured the maven-surefire-plugin to run my test classes in parallel and every Postgres container will use a random port.

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    <plugin>
     <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-surefire-plugin</artifactId>
      <configuration>
        <parallel>classes</parallel>
        <threadCount>2</threadCount>
      </configuration>
    </plugin>
  </plugins>
</build>

Overriding the properties for your application during the tests is as easy as the following snippet:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = IntegrationTest.Initializer.class)
public class IntegrationTest {

    @ClassRule
    public static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer().withPassword("inmemory")
            .withUsername("inmemory");

    public static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

        @Override
        public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
            TestPropertyValues values = TestPropertyValues.of(
                    "spring.datasource.url=" + postgreSQLContainer.getJdbcUrl(),
                    "spring.datasource.password=" + postgreSQLContainer.getPassword(),
                    "spring.datasource.username=" + postgreSQLContainer.getUsername()
            );
            values.applyTo(configurableApplicationContext);
        }
    }
}

If you use the @SpringBootTest annotation to spin-up the whole Spring container and the embedded Tomcat your Flyway scripts will be executed against the container and you verify them as a side-effect. Adding the @Sql annotation to a test method you can execute further SQL scripts before your tests and add e.g. sample data before your test. These changes will get rolled back after your test method exists.

@Test
@Sql("/insertPersons.sql")
public void testRestEndpointForAllPersons() {

  ResponseEntity<Person[]> result = testRestTemplate.getForEntity("http://localhost:" + localPort +
      "/persons", Person[].class);

  assertNotNull(result);
  assertThat(result.getBody().length, is(4));

}

For a full example with a sample Spring Boot application (simple CRUD REST application) visit my GitHub repository and have a look at the tests. I created three integration test classes (s (definitely not a best practice for your enterprise project) for creating, reading and deleting an entity to show you the parallel execution of the tests.

Happy integration-testing!

Leave a comment

Your email address will not be published. Required fields are marked *