Using Testcontainers for Database Testing with PostgreSQL and MySQL

Illustration for Using Testcontainers for Database Testing with PostgreSQL and MySQL
By Last updated:

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 or mysql: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.