Testcontainers runs real services (databases, message brokers, etc.) in Docker containers during tests — providing reliable integration testing without mocks.

Dependencies

  <dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.19.3</version>
    <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>
  

Requires Docker to be running on the test machine.

PostgreSQL Example

  @Testcontainers
class UserRepositoryIT {

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

    private UserRepository repository;

    @BeforeEach
    void setup() {
        DataSource ds = DataSourceBuilder.create()
            .url(postgres.getJdbcUrl())
            .username(postgres.getUsername())
            .password(postgres.getPassword())
            .build();
        repository = new UserRepository(ds);
        runMigrations(ds);
    }

    @Test
    void shouldSaveAndFindUser() {
        User user = new User("Alice", "[email protected]");
        repository.save(user);

        Optional<User> found = repository.findByEmail("[email protected]");
        assertTrue(found.isPresent());
        assertEquals("Alice", found.get().getName());
    }
}
  

Singleton Container Pattern

Start one container shared across all test classes:

  public abstract class AbstractIntegrationTest {

    static final PostgreSQLContainer<?> POSTGRES;

    static {
        POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine");
        POSTGRES.start();
    }
}
  

GenericContainer — Any Docker Image

  @Container
static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
    .withExposedPorts(6379);

@Test
void shouldConnectToRedis() {
    String host = redis.getHost();
    int port = redis.getMappedPort(6379);
    // connect with host:port
}
  

Kafka Example

  @Container
static KafkaContainer kafka = new KafkaContainer(
    DockerImageName.parse("confluentinc/cp-kafka:7.5.0"));

@Test
void shouldPublishAndConsume() {
    Properties props = new Properties();
    props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers());
    // produce and consume messages
}
  

Wait Strategies

Control when the container is considered ready:

  new GenericContainer<>("my-app:latest")
    .waitingFor(Wait.forHttp("/health").forStatusCode(200))
    .waitingFor(Wait.forLogMessage(".*Started.*", 1))
    .withStartupTimeout(Duration.ofMinutes(2));
  

Spring Boot Integration

  @SpringBootTest
@Testcontainers
class ApplicationIT {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");

    @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);
    }

    @Test
    void contextLoads() { }
}
  

Best Practices

  • Use @Container static for shared containers across tests in a class
  • Use the singleton pattern for containers shared across test classes
  • Pin Docker image versions — avoid latest tag
  • Use @DynamicPropertySource to inject container connection details into Spring
  • Run Testcontainers tests separately in CI (slower than unit tests)
  • Ensure Docker is available in CI (GitHub Actions: services or Docker-in-Docker)