Testing database-dependent code has always been one of the trickiest parts of software development. Traditional approaches often rely on in-memory databases like H2 or costly shared integration environments, both of which can introduce inconsistencies compared to real-world deployments.
This is where Testcontainers shines: it allows developers to spin up lightweight, disposable real database containers (e.g., PostgreSQL, MySQL) during test execution, ensuring high fidelity and reproducibility.
In this tutorial, we’ll walk through setting up PostgreSQL and MySQL Testcontainers in a Java project with JUnit 5, exploring use cases, configuration, best practices, and CI/CD integration.
What is Testcontainers?
Testcontainers is a Java library that provides lightweight, disposable Docker containers for integration testing.
It is widely used for testing databases, message brokers (Kafka, RabbitMQ), and cloud services in microservices and CI/CD pipelines.
Why use Testcontainers for Database Testing?
- ✅ Tests run against real PostgreSQL/MySQL instances, not simulators.
- ✅ Eliminates “works on my machine” issues by standardizing test environments.
- ✅ Supports parallel, isolated test runs.
- ✅ Works seamlessly with JUnit 5 and Spring Boot.
- ✅ Integrates with CI/CD pipelines on GitHub Actions, Jenkins, or GitLab CI.
Setting Up Testcontainers in a Java Project
Step 1: Add Maven/Gradle Dependencies
Maven:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.20.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.20.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>1.20.1</version>
<scope>test</scope>
</dependency>
Gradle:
testImplementation "org.testcontainers:junit-jupiter:1.20.1"
testImplementation "org.testcontainers:postgresql:1.20.1"
testImplementation "org.testcontainers:mysql:1.20.1"
Writing Your First PostgreSQL Test
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import static org.assertj.core.api.Assertions.assertThat;
public class PostgresContainerTest {
@Test
void testPostgresContainer() {
try (PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")) {
postgres.start();
assertThat(postgres.isRunning()).isTrue();
System.out.println("Postgres JDBC URL: " + postgres.getJdbcUrl());
}
}
}
Key Points:
postgres:15-alpine
is a lightweight official image.- Testcontainers automatically assigns random ports to avoid conflicts.
- Containers are automatically stopped and cleaned up.
Writing Your First MySQL Test
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.MySQLContainer;
import static org.assertj.core.api.Assertions.assertThat;
public class MySQLContainerTest {
@Test
void testMySQLContainer() {
try (MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")) {
mysql.start();
assertThat(mysql.isRunning()).isTrue();
System.out.println("MySQL JDBC URL: " + mysql.getJdbcUrl());
}
}
}
Integrating Testcontainers with JUnit 5
Instead of manually starting containers, use JUnit 5 extensions for lifecycle management:
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
class MyServiceTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("testdb")
.withUsername("testuser")
.withPassword("testpass");
@Test
void testServiceWithMySQL() {
System.out.println("Running MySQL at: " + mysql.getJdbcUrl());
}
}
Advanced Configuration
Preloading Data with Scripts
.withInitScript("init-db.sql")
Network Sharing Between Containers
Network network = Network.newNetwork();
PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine").withNetwork(network);
MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0").withNetwork(network);
Spring Boot Integration
With Spring Boot, Testcontainers can override spring.datasource.url
dynamically using:
spring:
datasource:
url: jdbc:tc:postgresql:15-alpine:///testdb
username: test
password: test
Best Practices for Database Testing with Testcontainers
- Use lightweight base images like
postgres:alpine
ormysql:8.0
. - Avoid long-running containers; start/stop per test class when possible.
- Use
@Testcontainers
with static containers for performance. - Store init SQL scripts in
src/test/resources
. - Run containers in parallel for faster builds.
- Integrate with CI/CD pipelines (GitHub Actions, Jenkins).
Real-World Use Case: Spring Boot + PostgreSQL
@SpringBootTest
@Testcontainers
class UserRepositoryTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("usersdb")
.withUsername("user")
.withPassword("pass");
@Autowired
private UserRepository userRepository;
@Test
void testSaveUser() {
User user = new User("John", "Doe");
userRepository.save(user);
assertThat(userRepository.findAll()).hasSize(1);
}
}
Version Tracker
- JUnit 4 → JUnit 5: Testcontainers moved from
@Rule
to@Container
annotations. - Mockito Updates: Modern Mockito now supports mocking static/final methods.
- Testcontainers Growth: Expanded support for PostgreSQL, MySQL, Kafka, LocalStack, Kubernetes.
Conclusion + Key Takeaways
- Testcontainers makes database testing realistic, isolated, and reproducible.
- PostgreSQL and MySQL containers integrate smoothly with JUnit 5 and Spring Boot.
- Advanced features like init scripts, shared networks, and CI/CD integration make it production-ready.
FAQ
1. What is the advantage of Testcontainers over H2 for testing?
H2 is an in-memory database that doesn’t fully mimic PostgreSQL/MySQL. Testcontainers runs real DB engines for accuracy.
2. Can I use Testcontainers in CI/CD pipelines?
Yes, Testcontainers works with Docker-based CI/CD systems like GitHub Actions, GitLab, and Jenkins.
3. Does Testcontainers work without Docker installed?
No, Docker (or Podman) must be installed on the host machine.
4. How do I preload schema/data into a Testcontainer?
Use .withInitScript("schema.sql")
or flyway/liquibase migrations.
5. Can I share one container across multiple test classes?
Yes, declare containers as static
with @Testcontainers
for reuse.
6. What if I need multiple databases running together?
Testcontainers supports multiple containers in the same Network
.
7. How do I speed up Testcontainers tests?
Use reusable containers with testcontainers.reuse.enable=true
in ~/.testcontainers.properties
.
8. Can I use Testcontainers with JUnit 4?
Yes, but JUnit 5 provides better lifecycle integration.
9. Does Testcontainers support cloud-native testing?
Yes, it integrates with LocalStack, Kubernetes, and cloud service emulators.
10. How does Testcontainers ensure cleanup?
Containers are automatically destroyed when the JVM exits or the test ends.