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:
- 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)
- The tests should not need any pre-setup before running (e.g. like manually setting up a test database)
- The tests should use my Flyway DDL scripts and create-drop (
spring.jpa.hibernate.ddl-auto
) shouldn't be activated for my tests - 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:
- Spring Boot Functional Tests with Selenium and Testcontainers
- Spring Boot Integration Tests with WireMock and JUnit 5
Happy integration-testing with Spring Boot, Testcontainers and JUnit,
Phil
Top comments (1)
Thanks for this, this helped me to get Spring Boot 2.4.5 working with Testcontainers and Jupiter!