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
- Download DockerToolbox-19.03.1.exe from https://github.com/docker/toolbox/releases/tag/v19.03.1
- Follow instruction: http://docs.docker.oeynet.com/toolbox/toolbox_install_windows
- 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) - Restarting windows is important, because it will not be able to find docker in Path (even if you add manually)
- 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:
- https://rieckpil.de/howto-write-spring-boot-integration-tests-with-a-real-database
- https://www.youtube.com/watch?v=-mYJKwrySOw
- https://www.testcontainers.org/test_framework_integration/junit_5
- https://www.testcontainers.org/test_framework_integration/junit_4
- https://github.com/rieckpil/blog-tutorials/tree/master/testcontainers-youtube-series
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:
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)