Testing with a real database is essential in complex applications. Imagine you're developing an app and using PostgreSQL in production. You write tests using H2, and everything works... until the first encounter with the real world, where the data schema causes an unexpected failure. That's exactly where Testcontainers helps.
Testcontainers is a Java library that lets you conveniently run isolated Docker containers for tests. It gives you the ability to spin up a real database (PostgreSQL, MySQL, MongoDB and others) in a container, test your app, and automatically destroy the container after the test finishes. No "bonuses" like leftover test junk on your local machine.
Advantages of Testcontainers
- Real behavior: using the same database as production minimizes the chance of surprises.
- Same environment everywhere: containers guarantee the same environment for tests on your laptop, CI/CD, and even your fridge if it supports Docker.
- Cleanliness: after the test the container is destroyed, leaving your system untouched.
- Support for many databases: PostgreSQL, MySQL, MariaDB, MongoDB, Cassandra and even Kafka.
Installing Testcontainers
Maven/Gradle dependencies
First let's add Testcontainers to the project. We'll use PostgreSQL as the example database.
Maven:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.19.0</version>
<scope>test</scope>
</dependency>
Gradle:
testImplementation("org.testcontainers:postgresql:1.19.0")
Simple setup of Testcontainers with PostgreSQL
Let's start by creating a basic test that uses a PostgreSQL container.
Step 1: Preparing the test container
Create a test class where we'll use the container. Testcontainers allows configuring PostgreSQL containers like this:
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
public class PostgresContainerTest {
@Test
void testPostgresContainer() {
try (PostgreSQLContainer
postgres = new PostgreSQLContainer<>("postgres:15")) {
// Start the container
postgres.start();
// Print container info for verification
System.out.println("Postgres URL: " + postgres.getJdbcUrl());
System.out.println("Username: " + postgres.getUsername());
System.out.println("Password: " + postgres.getPassword());
// You can connect to the DB via JDBC here and perform test actions
}
}
}
This test starts a Postgres 15 container. After calling start() the container comes up, and Testcontainers automatically finds an available port for the DB. After the try block finishes the container stops.
Step 2: Integrating with Spring Boot
Let's make Spring Boot use this container in tests. Configuration is done via @DynamicPropertySource.
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SpringBootTest
@Testcontainers
class DatabaseIntegrationTest {
@Container
private static final PostgreSQLContainer
POSTGRES = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void overrideProperties(org.springframework.test.context.DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", POSTGRES::getJdbcUrl);
registry.add("spring.datasource.username", POSTGRES::getUsername);
registry.add("spring.datasource.password", POSTGRES::getPassword);
}
@Test
void contextLoads() {
// Your test here
}
}
What's happening here?
- The
@Testcontainersannotation: indicates we're using Testcontainers. - The
@Containerannotation: automatically manages the container lifecycle (start/stop). @DynamicPropertySource: overridesspring.datasourceproperties so Spring uses the container's parameters.
Writing a repository test
Let's add a repository test that checks saving an entity to the DB.
Entity User:
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
// Getters and setters
}
Repository:
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
}
Test:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.junit.jupiter.Testcontainers;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@Testcontainers
class UserRepositoryTest {
@Container
private static final PostgreSQLContainer
POSTGRES = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void overrideProperties(org.springframework.test.context.DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", POSTGRES::getJdbcUrl);
registry.add("spring.datasource.username", POSTGRES::getUsername);
registry.add("spring.datasource.password", POSTGRES::getPassword);
}
@Autowired
private UserRepository userRepository;
@Test
void testSaveUser() {
User user = new User();
user.setName("John Doe");
User savedUser = userRepository.save(user);
assertThat(savedUser.getId()).isNotNull();
assertThat(savedUser.getName()).isEqualTo("John Doe");
}
}
This test spins up the DB container, hooks it into Spring Boot, and lets you test saving an entity without manually bringing up Postgres.
Pitfalls and common issues
- Docker must be installed and running: if Docker isn't running, your tests will simply refuse to start.
- Port 5432 already taken? Testcontainers automatically uses a random host port for the container, so port conflicts are avoided.
- Slow initialization? the first run can take longer because the image needs to be pulled from Docker Hub.
Practical use
- Real testing environment: you can be confident your app will behave the same in production as in tests.
- CI/CD automation: adding Testcontainers to pipelines (for example, GitHub Actions or GitLab CI) makes integration tests more reliable.
- Dynamic testing of different configurations: just change container parameters (for example, Postgres version) and verify everything works correctly.
Testcontainers is a powerful tool for integration testing with real databases that makes your testing process more reliable and closer to real-world operation. Learn to use it, and remember that containers should still be shut down after tests... Good thing Testcontainers does that automatically! 😉
GO TO FULL VERSION