loading...

Write Spring Boot integration tests with Testcontainers (JUnit 4 & 5)

rieckpil profile image Philip Riecks Originally published at rieckpil.de ・4 min read

Recently, I was looking for a solution to write integration tests for my Spring Boot-based application which was using PostgreSQL. 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 use my Flyway DDL scripts and create-drop (spring.jpa.hibernate.ddl-auto) shouldn't be activated for my tests
  4. Good integration with the excellent Spring tests ecosystem

For this task, I found the awesome project: Test containers. 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."

With this blog post, we'll use Testcontainers to write integration tests with JUnit using a real database (meaning not mocked or in-memory) for a Spring Boot application.

UPDATE: Time flies and a lot was introduced since I published this blog post. Therefore I added integration test examples for different combinations of JUnit 4 & 5 and Spring Boot versions.

Setup Testcontainers in Spring Boot project

For using this dependency you need to have Docker on your local machine/on your build server (Jenkins etc.).

With Testcontainers you can use a @ClassRule or @Rule on each of your integration tests and define the Docker image for your test (valid for JUnit 4.12).

For MySQL and PostgreSQL and there are already built-in solutions but you are free to use an 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 run the integrations tests after your unit tests, simply add maven-failsafe-plugin to your project. In addition, make sure your integration tests have IT as a postfix:

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-failsafe-plugin</artifactId>
      <version>3.0.0-M4</version>
      <executions>
        <execution>
          <goals>
            <goal>integration-test</goal>
            <goal>verify</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

Basic application integration test with Testcontainers

Using: JUnit 4.12 and Spring Boot < 2.2.6

Let's start with the integration test each Spring Boot application contains out-of-the-box. This integration test verifies that Spring can create the context and start the application.

As our application requires a PostgreSQL to be available during startup, we can provide one using Testcontainers. Overriding the properties to use the PostgreSQL database spawned by Testcontainers is as easy as the following:

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

    @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);
        }
    }

    @Ε¦est
    public void contextLoads() {
    }
}

Basic application integration test with JUnit 5 and Spring Boot > 2.2.6

If your application uses JUnit 5, you can't use the @ClassRule anymore. Fortunately, Testcontainers provides a solution to write tests with JUnit Jupiter:

<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>${testcontainers.version}</version>
  <scope>test</scope>
</dependency>

With this dependency and a more recent version of Spring Boot (> 2.2.6) the basic integration test looks like the following:

// JUnit 5 example with Spring Boot >= 2.2.6
@Testcontainers
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class ApplicationIT {

  @Container
  public static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer()
    .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);
  }

  @Test
  public void contextLoads() {
  }

}

Integration test with JUnit 5 and Spring Boot < 2.2.6

If your application makes use of JUnit 5 but is using a Spring Boot version < 2.2.6, you don't have access to the @DynamicPropertySource feature.

A possible integration test to verify a REST API endpoint is working as expected looks like the following:

// JUnit 5 example with Spring Boot < 2.2.6
@Testcontainers
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = DeletePersonIT.Initializer.class)
public class DeletePersonIT {

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

  @Autowired
  private PersonRepository personRepository;

  @Autowired
  public TestRestTemplate testRestTemplate;

  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);
    }
  }

  @Test
  @Sql("/testdata/FILL_FOUR_PERSONS.sql")
  public void testDeletePerson() {
    testRestTemplate.delete("/api/persons/1");
    assertEquals(3, personRepository.findAll().size());
    assertFalse(personRepository.findAll().contains("Phil"));

  }
}

You can find more integration test examples for this demo Spring Boot CRUD API application using PostgreSQL on GitHub.

Further integration test-related tutorials for Spring Boot:

Happy integration-testing with Spring Boot, Testcontainers and JUnit,
Phil

Discussion

pic
Editor guide