DEV Community

Philip Riecks
Philip Riecks

Posted on • Originally published at rieckpil.de

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

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 (1)

Collapse
lintong profile image
Linton Galloway

Thanks for this, this helped me to get Spring Boot 2.4.5 working with Testcontainers and Jupiter!