~/blog/testcontainers-integration-testing
×

> cat --date="2025-08-10" article.md

Testcontainers: Integration Testing Without the Pain

Integration tests have a reputation for being slow, flaky, and hard to maintain. Most of that reputation comes from one problem: managing test infrastructure. Testcontainers solves this elegantly.

The Old Way

Before Testcontainers, your options were:

  1. H2 in-memory database — fast but behavior differs from production PostgreSQL
  2. Shared test database — tests interfere with each other, CI becomes a bottleneck
  3. Docker Compose scripts — complex setup, hard to keep in sync with code

None of these are satisfactory for a serious project.

Enter Testcontainers

Testcontainers spins up real Docker containers for your tests. Each test class (or test suite) gets its own isolated infrastructure:

@SpringBootTest
@Testcontainers
class OrderRepositoryTest {

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

    @DynamicPropertySource
    static void configure(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 OrderRepository orderRepository;

    @Test
    void shouldPersistAndRetrieveOrder() {
        Order order = new Order("ITEM-001", BigDecimal.valueOf(99.99));
        orderRepository.save(order);

        Optional<Order> found = orderRepository.findById(order.getId());

        assertThat(found).isPresent();
        assertThat(found.get().getItemCode()).isEqualTo("ITEM-001");
    }
}

Beyond Databases

Testcontainers supports dozens of modules:

  • Kafka — test event-driven architectures with a real broker
  • Redis — verify caching behavior against the actual engine
  • LocalStack — simulate AWS services (S3, SQS, DynamoDB) locally
  • Elasticsearch — test full-text search with production-grade indexing

Performance Tips

  • Use @Container with static fields to share containers across tests in a class
  • Use reusable containers (withReuse(true)) during local development
  • Run tests in parallel with separate containers for true isolation

The Result

After adopting Testcontainers across three projects, our flaky test rate dropped from ~8% to near zero. Build times increased by 30 seconds (container startup), but we eliminated hours of debugging false failures.

The trade-off is overwhelmingly positive.

> cd ../blog > cd ~