CodeGym /Courses /Module 5. Spring /Lecture 140: Using Testcontainers to test with a real dat...

Lecture 140: Using Testcontainers to test with a real database

Module 5. Spring
Level 9 , Lesson 9
Available

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")
Important:
To use Testcontainers, Docker must be installed and running on your machine.

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?

  1. The @Testcontainers annotation: indicates we're using Testcontainers.
  2. The @Container annotation: automatically manages the container lifecycle (start/stop).
  3. @DynamicPropertySource: overrides spring.datasource properties 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

  1. Real testing environment: you can be confident your app will behave the same in production as in tests.
  2. CI/CD automation: adding Testcontainers to pipelines (for example, GitHub Actions or GitLab CI) makes integration tests more reliable.
  3. 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! 😉

Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION