Spring Boot 3.2 + Testcontainers: Reliable Integration Testing with Real Dependencies

작성자

카테고리:

← 피드로
DEV Community · Nitish · 2026-06-19 개발(SW)

Nitish

Spring Boot 3.2 + Testcontainers: Reliable Integration Testing with Real Dependencies

Without Testcontainers, your integration tests might pass locally with H2 but fail in CI when connecting to a mismatched PostgreSQL version. Production database migrations succeed in development but break when applied to a different database engine.

Prerequisites

  • Java 17 or later
  • Spring Boot 3.2.x
  • Docker Engine 24.0+ with Docker Compose
  • JUnit Jupiter 5.9+
  • Testcontainers 1.19.3

Configuring Testcontainers for Database Testing

Testcontainers provides disposable database instances for integration tests. Add these dependencies to your pom.xml:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.4</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>1.19.3</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>postgresql</artifactId>
        <version>1.19.3</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Enter fullscreen mode Exit fullscreen mode

Disable the default datasource in test configuration:

# src/test/resources/application.properties
spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.datasource.url=jdbc:tc:postgresql:15-alpine:///test?TC_DAEMON=true
spring.flyway.enabled=false

Enter fullscreen mode Exit fullscreen mode

Writing a Containerized Integration Test

This test verifies database schema initialization using a real PostgreSQL instance. The container starts before tests and destroys itself afterward:

package com.example.orderservice;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import static org.assertj.core.api.Assertions.assertThat;

@Testcontainers
@SpringBootTest
class OrderRepositoryIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
        .withDatabaseName("test")
        .withUsername("test")
        .withPassword("test");

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Test
    void should_connect_to_live_database() {
        Integer result = jdbcTemplate.queryForObject("SELECT 1", Integer.class);
        assertThat(result).isEqualTo(1);
    }
}

Enter fullscreen mode Exit fullscreen mode

Run the test with Docker Desktop active. Testcontainers will download the PostgreSQL image on first execution and reuse it for subsequent runs.

Managing Container Lifecycles

Define a base test class to share containers across multiple test classes:

package com.example.shared;

import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
public abstract class BaseIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
        .withReuse(true);

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }
}

Enter fullscreen mode Exit fullscreen mode

Common Mistakes

Mistake 1: Non-static container field causing multiple instances

@Container // Missing static modifier
PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>();

Enter fullscreen mode Exit fullscreen mode

@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>();

Enter fullscreen mode Exit fullscreen mode

Testcontainers requires static fields for class-level containers. Non-static fields create new containers per test method.

Mistake 2: Hardcoded JDBC URL in test properties

spring.datasource.url=jdbc:postgresql://localhost:5432/test

Enter fullscreen mode Exit fullscreen mode

@DynamicPropertySource
static void registerProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", postgres::getJdbcUrl);
}

Enter fullscreen mode Exit fullscreen mode

Hardcoded URLs bypass container networking. Use @DynamicPropertySource to inject runtime connection details.

Mistake 3: Missing container reuse configuration

new PostgreSQLContainer<>("postgres:15-alpine");

Enter fullscreen mode Exit fullscreen mode

new PostgreSQLContainer<>("postgres:15-alpine")
    .withReuse(true);

Enter fullscreen mode Exit fullscreen mode

Without reuse, Testcontainers destroys containers after each test class. Enable reuse in .testcontainers.properties and chain withReuse(true).

Summary

  • Replace in-memory databases with Testcontainers-managed PostgreSQL instances using @Testcontainers and @Container
  • Inject dynamic connection properties using @DynamicPropertySource registry
  • Enable container reuse with .withReuse(true) and testcontainers.reuse.enable=true in .testcontainers.properties

The author publishes Spring Boot starter templates at https://gumroad.com

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다