Modern software applications rarely live in isolation—they interact with databases, message queues, or cloud services. Writing reliable tests for these systems can be tricky when depending on local or shared environments. Testcontainers solves this problem by allowing you to spin up lightweight, disposable Docker containers for databases and other services directly from your test code.
In this tutorial, you’ll learn how to write your first test with Testcontainers using JUnit 5, explore its importance in real-world applications, and understand best practices for maintainable test suites.
What is Testcontainers?
Testcontainers is a Java library that provides throwaway containers for integration testing. Instead of relying on external test databases, you can launch real instances of PostgreSQL, MySQL, Kafka, Redis, and more—all orchestrated automatically within your test suite.
Key benefits include:
- Reproducibility: Every test runs in a clean environment.
- CI/CD friendly: Works seamlessly in Jenkins, GitHub Actions, and GitLab pipelines.
- Supports microservices: Spin up dependent services like message brokers or APIs.
- Lightweight and fast: Containers start only when needed and shut down after tests.
Why Use Testcontainers?
- Bug prevention: Avoids false positives from mismatched environments.
- Maintainability: Keeps tests independent of external shared resources.
- Scalability: Perfect for microservices and cloud-native applications.
- CI/CD reliability: No need for pre-installed databases in your pipeline.
👉 Think of Testcontainers as a disposable lab environment—you can run experiments (tests) without polluting your real setup.
Setting Up Dependencies
Before writing our first test, let’s configure Maven (Gradle works similarly).
<dependencies>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<!-- Testcontainers Core -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.20.1</version>
<scope>test</scope>
</dependency>
<!-- Testcontainers PostgreSQL -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.20.1</version>
<scope>test</scope>
</dependency>
<!-- PostgreSQL JDBC Driver -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.2</version>
<scope>test</scope>
</dependency>
</dependencies>
Make sure Docker is installed and running on your machine.
Writing Your First Test with Testcontainers
Let’s write a JUnit 5 integration test using PostgreSQL as an example.
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Testcontainers
public class PostgresTestcontainersTest {
@Container
private static final PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("secret");
@Test
void testDatabaseConnection() throws Exception {
try (Connection connection = DriverManager.getConnection(
postgres.getJdbcUrl(),
postgres.getUsername(),
postgres.getPassword())) {
Statement stmt = connection.createStatement();
stmt.execute("CREATE TABLE users(id SERIAL PRIMARY KEY, name VARCHAR(255));");
stmt.execute("INSERT INTO users(name) VALUES ('Alice');");
ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM users;");
rs.next();
int count = rs.getInt(1);
assertEquals(1, count, "There should be one user in the database.");
}
}
}
Explanation:
@Testcontainers
tells JUnit 5 to manage containers.@Container
ensures the PostgreSQL container lifecycle is tied to the test class.- A real PostgreSQL instance is spun up via Docker.
- We connect using JDBC, create a table, insert data, and assert results.
✅ This is a real integration test—not a mock.
Testcontainers Logs
When you run the test, you’ll see logs like:
Starting container: postgres:15
Container postgres:15 started in 6.78s
This confirms the container was launched dynamically for the test.
Best Practices for Your First Testcontainers Tests
- Keep tests isolated: Each test should prepare its own data.
- Use reusable containers: Use
@Container
static fields for efficiency. - Leverage wait strategies: Ensure services are ready before tests run.
- Integrate with Spring Boot: Combine with
@DynamicPropertySource
for easy configuration. - CI/CD pipelines: Make sure Docker is available in your build runners.
Version Tracker
- JUnit 4 → JUnit 5: Modern annotations and extension model.
- Mockito updates: Now supports static/final mocking.
- Testcontainers evolution: Expanded modules for Kafka, Redis, Elasticsearch, LocalStack, and Kubernetes.
Conclusion + Key Takeaways
- Testcontainers = real, disposable test environments.
- Perfect for testing databases, message brokers, and cloud services.
- Your first test involves just a few lines of code.
- Integrates seamlessly with JUnit 5, Spring Boot, and CI/CD pipelines.
With Testcontainers, your tests will be more reliable, production-like, and CI/CD ready.
FAQ
1. What is the difference between unit and integration tests?
Unit tests validate small pieces of logic, while integration tests validate interactions with external systems like databases.
2. Do I need Docker installed to run Testcontainers?
Yes. Docker is required since containers run in isolated environments.
3. Can I use Testcontainers with JUnit 4?
Yes, but JUnit 5 provides a cleaner integration via annotations.
4. How can I speed up container startup?
Reuse containers across tests or enable Testcontainers' Ryuk cleanup feature.
5. Can I use Testcontainers in CI/CD?
Absolutely. Works in GitHub Actions, Jenkins, GitLab CI, and more.
6. What happens if Docker isn’t available?
Your tests will fail. Use assumptions (Assumptions.assumeTrue
) to skip tests in such cases.
7. Is Testcontainers only for databases?
No, it supports message brokers (Kafka, RabbitMQ), search engines (Elasticsearch), cloud emulators (LocalStack), and more.
8. How does Testcontainers compare to in-memory databases like H2?
Testcontainers runs the real database, avoiding differences between production and tests.
9. Can I use Testcontainers with Spring Boot?
Yes, integrate with @DynamicPropertySource
to configure properties at runtime.
10. Is there a performance tradeoff?
Containers add startup time, but reliability and accuracy outweigh this for integration tests.