How to Use Testcontainers in Integration Tests

Author:  Oleksandr_Dekin

Introduction

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.

Documentation: https://www.testcontainers.org

Prerequisites: 

  • Docker 
  • JUnit 4 or JUnit 5 ( with Jupiter)

Installation docker on windows

  1. Download DockerToolbox-19.03.1.exe from https://github.com/docker/toolbox/releases/tag/v19.03.1
  2. Follow instruction:  http://docs.docker.oeynet.com/toolbox/toolbox_install_windows
  3. During installation unselect component VirtualBox Follow http://docs.docker.oeynet.com/toolbox/toolbox_install_windows
    NOTE: you don't need to install VirtualBox if you have installed before it (for example: you are working with vagrant and use VB)
  4. Restarting windows is important, because it will not be able to find docker in Path (even if you add manually)
  5. After install, on you desktop will be created shortcut shortcut for docker - Docker Quickstart Terminal

Setup Testcontainers in Spring Boot

  • Integration test with JUnit 4.12 and Spring Boot < 2.2.6

    Using: JUnit 4.12 and Spring Boot < 2.2.6
    @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() {
        }
    }
  • 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
    // 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"));
     
      }
    }


  • 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:

    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
    // 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() {
      }
    }

Useful links:

Inheritance in Testcontainers

  • For reuse logic we can create abstract and define container and configure dynamic property source. For example:

    BaseIntegrationTest
    @Testcontainers
    @SpringBootTest
    @AutoConfigureMockMvc
    @DirtiesContext
    public abstract class BaseIntegrationTest {
    
      private static final String IMAGE_VERSION = "postgres:12-alpine";
      public static final String MOD_OPAC_AUTH_MODULE = "mod-opac-auth-1.0.0";
    
      @Autowired
      private MockMvc mockMvc;
    
      @Container
      public static PostgreSQLContainer<?> postgreSQLContainer = new PostgreSQLContainer<>(IMAGE_VERSION);
    
      @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);
      }
    ....
    ....
    }
  • Now, we can extend and write test:

    FooControllerIT
    @Log4j2
    class FooControllerIT extends BaseIntegrationTest {
    
      @Test
      void testFoo() throws Exception {
        //...
      }
      
      //...
    }

NOTE: @DirtiesContext - SpringBootTest is reusing Spring context between tests so there is a common Hikari Pool between tests. But in the background testcontainers killed (after the previous test) a container and created a new one (before the next test). SpringBootTest is not aware of that change resulting in a new Postgres container so Hikari Pool is the same as in the previous test (pointing to already used and currently unavailable port). For reusing testcontainers we can add annotation @DirtiesContext


Run integration test

There are several option to run integration tests:

  • Run automatically as part of `mvn clean install`
  • Run via IntelliJ IDEA

After run test, you can find in the docker terminal that several test containers will be created for integration tests:

NOTE: if you use Junit 5, you don't need to stop container in tne code, because PostgreSQLContainer calss implement interfacce AutoCloseable which stop container (https://www.testcontainers.org/test_framework_integration/manual_lifecycle_control)