JAVA Contents

Testcontainers, DB integration

Run real integration tests with Testcontainers: validate database behavior, migrations and transactional correctness under realistic conditions.

On this page

Why Integration Tests Exist

Unit tests validate logic. Integration tests validate reality. Reality includes: - database constraints - transaction behavior - migrations - serialization - connection pooling settings - query performance shape If you never test reality, production becomes your integration environment.

Incident Scenario: Migration Broke Writes, Unit Tests Stayed Green

A migration added a column without default. The app wrote null and failed at runtime. Unit tests mocked repository and passed. Integration tests would have failed immediately.

Testcontainers: Production-Like Dependencies in CI

Testcontainers runs real dependencies in containers during tests: - PostgreSQL/MySQL - Redis - Kafka - LocalStack This allows deterministic, repeatable integration testing in CI.

Anti-Pattern: Using Shared Dev Database for Tests

Shared databases cause: - nondeterministic failures - data leaks across tests - environment coupling - painful cleanup Integration tests must own their environment.

Spring Boot + Testcontainers Example

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@SpringBootTest
@Testcontainers
class UserRepositoryIT {

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

    @Test
    void shouldPersistAndLoadUser() {
        // real DB verification happens through repository
    }
}
In real projects, wire container properties into Spring datasource configuration.

What to Validate With Integration Tests

High-value targets: - migrations apply cleanly - constraints behave as expected (unique, not null, FK) - transaction rollback works - repository queries return correct data - N+1 detection for critical flows (at least on key endpoints) - serialization compatibility for stored JSON

Transactional Correctness Testing

You must test: - rollback on exceptions - idempotency boundaries - concurrency anomalies (where relevant) If concurrency matters, write targeted tests, not random sleeps.

Keep Integration Tests Focused

Do not turn integration tests into end-to-end UI tests. Focus on boundaries that unit tests cannot validate. Rules: - Use real DB and migrations - Keep dataset minimal but representative - Avoid long-running tests - Run in CI with caching of container layers

Checklist

- Use Testcontainers for real dependencies - Never use shared dev DB for tests - Validate migrations and constraints - Test transaction boundaries and rollback - Keep tests deterministic and fast enough for CI - Treat integration tests as production risk reducers If you ship without integration tests, you are betting production will be stable. That bet is usually wrong.