Setting Up Testcontainers in a Java Project: A Complete Guide for Java Developers

Illustration for Setting Up Testcontainers in a Java Project: A Complete Guide for Java Developers
By Last updated:

Modern Java applications often depend on databases, message brokers, or external services. Testing them reliably in real-world environments is challenging. That’s where Testcontainers shines.
By running lightweight, disposable containers directly from your tests, Testcontainers provides reproducible environments, eliminating the “it works on my machine” issue.

In this tutorial, we’ll cover how to set up Testcontainers in a Java project, from dependencies to writing your first containerized test.


What is Testcontainers?

Testcontainers is a Java library that uses Docker to run databases, queues, and other services in containers for testing. Instead of mocking everything or relying on shared test servers, you spin up real, isolated instances of PostgreSQL, MySQL, Kafka, or Redis right in your test lifecycle.

  • JUnit 5 Integration: Annotated lifecycle support for containers.
  • Wide Ecosystem: Supports databases, queues, cloud services.
  • CI/CD Friendly: Runs the same way locally and in pipelines.

Adding Testcontainers to Your Project

Maven Dependency

<dependencies>
    <!-- JUnit 5 -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.10.0</version>
        <scope>test</scope>
    </dependency>

    <!-- Testcontainers BOM for version alignment -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.testcontainers</groupId>
                <artifactId>testcontainers-bom</artifactId>
                <version>1.19.3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <!-- PostgreSQL Testcontainer -->
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>postgresql</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Gradle Dependency

testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
testImplementation 'org.testcontainers:junit-jupiter:1.19.3'
testImplementation 'org.testcontainers:postgresql:1.19.3'

Writing Your First Test with Testcontainers

Here’s how to set up a PostgreSQL container and test against it:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
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 PostgresContainerTest {

    @Container
    public static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
            .withDatabaseName("testdb")
            .withUsername("user")
            .withPassword("password");

    @Test
    void testDatabaseConnection() throws Exception {
        try (Connection conn = DriverManager.getConnection(
                postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword())) {
            Statement stmt = conn.createStatement();
            stmt.execute("CREATE TABLE items(id SERIAL PRIMARY KEY, name VARCHAR(100));");
            stmt.execute("INSERT INTO items(name) VALUES ('apple');");

            ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM items;");
            rs.next();
            int count = rs.getInt(1);

            assertEquals(1, count, "Should have inserted one item");
        }
    }
}

Key Points:

  • @Testcontainers initializes container lifecycle.
  • @Container ensures container start/stop with tests.
  • Provides real PostgreSQL for integration testing.

Integrating Testcontainers with Spring Boot

If you’re working with Spring Boot, you can use DynamicPropertySource to inject Testcontainers properties:

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;

@SpringBootTest
class SpringBootTestWithPostgres {

    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");

    static {
        postgres.start();
    }

    @DynamicPropertySource
    static void overrideProperties(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 Spring Boot context test
    }
}

Best Practices for Testcontainers Setup

  1. Use Reusable Containers – Use withReuse(true) in dev environments.
  2. Keep Tests Isolated – Each test should not depend on external state.
  3. Optimize CI/CD – Cache Docker images to speed up pipeline builds.
  4. Combine with Mockito – Use Testcontainers for real dependencies, Mockito for external APIs.
  5. Version Pinning – Always use a specific Docker image tag (e.g., postgres:15).

Version Tracker

  • JUnit 4 → JUnit 5: Native support for lifecycle annotations.
  • Mockito: Gained static/final method mocking support.
  • Testcontainers: Expanded to cloud-native services (LocalStack, Kafka, MongoDB).

Conclusion & Key Takeaways

  • Testcontainers allows developers to run real databases and services in tests.
  • Setup is easy with Maven/Gradle and integrates with JUnit 5.
  • Supports Spring Boot and CI/CD pipelines.
  • Encourages reliable, production-like tests without mocks.

FAQ

1. What is the difference between unit and integration tests?
Unit tests check isolated methods; integration tests verify components working together (often with Testcontainers).

2. Do I always need Docker installed for Testcontainers?
Yes, Docker (or Podman) is required because Testcontainers spins up containers.

3. Can I mock static methods in Mockito?
Yes, since Mockito 3.4+, static method mocking is supported.

4. How do I fix flaky Testcontainers tests?
Ensure proper cleanup, use fixed Docker image tags, and avoid port conflicts.

5. How does Testcontainers help in CI/CD pipelines?
It ensures consistent environments across developer machines and CI servers.

6. Can I use Testcontainers for microservices?
Yes, you can spin up Kafka, RabbitMQ, or even whole Docker Compose setups.

7. What’s the difference between TDD and BDD?
TDD focuses on writing tests before code, BDD emphasizes behavior-driven specifications.

8. How can I measure test coverage in Testcontainers-based projects?
Use JaCoCo with Maven/Gradle.

9. Does Testcontainers work with Kubernetes?
Yes, you can use it to spin up local dependencies or connect to external clusters.

10. Can Testcontainers replace mocks entirely?
Not always — use it for databases/infra, but Mockito for lightweight service mocks.