This quick starter will guide you to configure and use Testcontainers in a SpringBoot project.
In this guide, we'll look at a sample Spring Boot application that uses Testcontainers for running unit tests with real dependencies. The initial implementation uses a relational database for storing data. We'll look at the necessary parts of the code that integrates Testcontainers into the app. Then we'll switch the relation database for MongoDB, and guide you through using Testcontainers for testing the app against a real instance of MongoDB running in a container.
After the quick start, you'll have a working Spring Boot app with Testcontainers-based tests, and will be ready to explore integrations with other databases and other technologies via Testcontainers.
Make sure you have Java 8+ and a compatible Docker environment installed. If you are going to use Maven build tool then make sure Java 17+ is installed.
For example:
$ java -version
openjdk version "17.0.4" 2022-07-19
OpenJDK Runtime Environment Temurin-17.0.4+8 (build 17.0.4+8)
OpenJDK 64-Bit Server VM Temurin-17.0.4+8 (build 17.0.4+8, mixed mode, sharing)
$ docker version
...
Server: Docker Desktop 4.12.0 (85629)
Engine:
Version: 20.10.17
API version: 1.41 (minimum version 1.12)
Go version: go1.17.11
...
- Clone the repository
git clone https://github.com/testcontainers/testcontainers-java-spring-boot-quickstart.git && cd testcontainers-java-spring-boot-quickstart
- Open the testcontainers-java-spring-boot-quickstart project in your favorite IDE.
The sample project uses JUnit tests and Testcontainers to run them against actual databases running in containers.
Run the command to run the tests.
$ ./gradlew test //for Gradle
$ ./mvnw verify //for Maven
The tests should pass.
The testcontainers-java-spring-boot-quickstart project is a SpringBoot REST API using Java 17, Spring Data JPA, PostgreSQL, and Gradle/Maven. We are using JUnit 5, Testcontainers and RestAssured for testing.
Following are the Testcontainers and RestAssured dependencies:
build.gradle
ext {
set('testcontainersVersion', "1.18.1")
}
dependencies {
...
...
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:postgresql'
testImplementation 'io.rest-assured:rest-assured'
}
dependencyManagement {
imports {
mavenBom "org.testcontainers:testcontainers-bom:${testcontainersVersion}"
}
}
For Maven build the Testcontainers and RestAssured dependencies are configured in pom.xml as follows:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<properties>
...
...
<testcontainers.version>1.18.1</testcontainers.version>
</properties>
<dependencies>
...
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>${testcontainers.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Testcontainers library can be used to spin up desired services as docker containers and run tests against those services. We can use our testing library lifecycle hooks to start/stop containers using Testcontainers API.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class TodoControllerTests {
@LocalServerPort
private Integer port;
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine");
@BeforeAll
static void beforeAll() {
postgres.start();
}
@AfterAll
static void afterAll() {
postgres.stop();
}
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
TodoRepository todoRepository;
@BeforeEach
void setUp() {
todoRepository.deleteAll();
RestAssured.baseURI = "http://localhost:" + port;
}
@Test
void shouldGetAllTodos() {
List<Todo> todos = List.of(
new Todo(null, "Todo Item 1", false, 1),
new Todo(null, "Todo Item 2", false, 2)
);
todoRepository.saveAll(todos);
given()
.contentType(ContentType.JSON)
.when()
.get("/todos")
.then()
.statusCode(200)
.body(".", hasSize(2));
}
}
Here we have defined a PostgreSQLContainer
instance, started the container before executing tests and stopped it after executing all the tests using JUnit 5 test lifecycle hook methods.
Note
If you are using any different Testing library like TestNG or Spock then you can use similar lifecycle callback methods provided by that testing library.
The Postgresql container port (5432) will be mapped to a random available port on the host.
This helps to avoid port conflicts and allows running tests in parallel.
Then we are using SpringBoot's dynamic property registration support to add/override the datasource
properties obtained from the Postgres container.
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
In shouldGetAllTodos()
test we are saving two Todo entities into the database using TodoRepository
and testing GET /todos
API endpoint to fetch todos using RestAssured.
You can run the tests directly from IDE or using the command ./gradlew test
from the terminal.
Instead of implementing JUnit 5 lifecycle callback methods to start and stop the Postgres container, we can use Testcontainers JUnit 5 Extension annotations to manage the container lifecycle as follows:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
public class TodoControllerTests {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
}
Note
The Testcontainers JUnit 5 Extension will take care of starting the container before tests and stopping it after tests. If the container is a
static
field then it will be started once before all the tests and stopped after all the tests. If it is a non-static field then the container will be started before each test and stopped after each test.Even if you don't stop the containers explicitly, Testcontainers will take care of removing the containers, using
ryuk
container behind the scenes, once all the tests are done. But it is recommended to clean up the containers as soon as possible.
Testcontainers provides the special jdbc url support which automatically spins up the configured database as a container.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties = {
"spring.datasource.url=jdbc:tc:postgresql:15-alpine:///todos"
})
class ApplicationTests {
@Test
void contextLoads() {
}
}
By setting the datasource url to jdbc:tc:postgresql:15-alpine:///todos
(notice the special :tc
prefix),
Testcontainers automatically spin up the Postgres database using postgresql:15-alpine
docker image.
For more information on Testcontainers JDBC Support refer https://www.testcontainers.org/modules/databases/jdbc/
Spring Boot 3.1.0 introduced better support for Testcontainers that simplifies test configuration greatly.
Instead of registering the postgres database connection properties using @DynamicPropertySource
,
we can use @ServiceConnection
to register the Database connection as follows:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
public class TodoControllerTests {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine");
@Test
void test() {
...
}
}
Spring Boot 3.1.0 introduced support for using Testcontainers at development time. You can configure your Spring Boot application to automatically start the required docker containers.
First, create a configuration class to define the required containers as follows:
@TestConfiguration(proxyBeanMethods = false)
public class ContainersConfig {
@Bean
@ServiceConnection
@RestartScope
PostgreSQLContainer<?> postgreSQLContainer(){
return new PostgreSQLContainer<>("postgres:15-alpine");
}
}
Next, create a TestApplication
class under src/test/java
as follows:
public class TestApplication {
public static void main(String[] args) {
SpringApplication
.from(Application::main)
.with(ContainersConfig.class)
.run(args);
}
}
Now you can either run TestApplication
from your IDE or use your build tool to start the application as follows:
$ ./gradlew bootTestRun //for Gradle
$ ./mvnw spring-boot:test-run //for Maven
You can access the application UI at http://localhost:8080 and enter http://localhost:8080/todos as API URL.
During development, you can use Spring Boot DevTools to reload the code changes without having to completely restart the application.
You can also configure your containers to reuse the existing containers by adding @RefreshScope
.
First, Add spring-boot-devtools
dependency.
Gradle
testImplementation 'org.springframework.boot:spring-boot-devtools'
Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
Next, add @RefreshScope
annotation on container bean definition as follows:
@TestConfiguration(proxyBeanMethods = false)
public class ContainersConfig {
@Bean
@ServiceConnection
@RestartScope
PostgreSQLContainer<?> postgreSQLContainer(){
return new PostgreSQLContainer<>("postgres:15-alpine");
}
}
Now when devtools reloads your application, the same containers will be reused instead of re-creating them.