DEV Community

Nitish
Nitish

Posted on

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

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

Top comments (0)