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
@DynamicPropertySourcefeature.
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
@ClassRuleanymore. 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.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:
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)