Spring Boot Testcontainers Cloud: Scalable Integration Testing in CI/CD

Spring Boot Testcontainers Cloud for Integration Testing

Testcontainers Cloud integration testing has become the gold standard for verifying Spring Boot applications against real databases, message brokers, and external services. While local Testcontainers work great on developer machines, running them in CI/CD pipelines presents significant challenges — Docker-in-Docker complexity, resource constraints, and slow startup times. Testcontainers Cloud solves these problems by offloading container execution to managed cloud infrastructure.

This guide covers everything from setting up Testcontainers Cloud with Spring Boot 3.x to optimizing parallel test execution in GitHub Actions and GitLab CI. Moreover, you will learn patterns for managing test data, handling service dependencies, and reducing overall pipeline execution time by 60% or more.

Why Traditional Testcontainers Struggle in CI

Local Testcontainers rely on a Docker daemon running alongside your tests. In CI environments, this typically means Docker-in-Docker (DinD) which adds security risks and performance overhead. Additionally, CI runners often have limited CPU and memory, making it difficult to spin up multiple containers simultaneously.

Furthermore, each CI job starts fresh — pulling container images from registries every time. A typical Spring Boot test suite that needs PostgreSQL, Redis, and Kafka containers can spend 2-3 minutes just on container startup before any tests run.

Testcontainers Cloud integration testing architecture
Testcontainers Cloud offloads container execution to managed infrastructure

Setting Up Testcontainers Cloud

Getting started requires a Testcontainers Cloud account and a lightweight agent that redirects Docker API calls to the cloud. The agent runs as a background process on your CI runner and transparently intercepts container lifecycle operations.

<!-- pom.xml dependencies -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-testcontainers</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers-bom</artifactId>
    <version>1.20.4</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>kafka</artifactId>
    <scope>test</scope>
</dependency>

Testcontainers Cloud: Spring Boot Service Connections

Spring Boot 3.1+ introduced @ServiceConnection which automatically configures connection properties from Testcontainers. This eliminates manual property mapping and reduces boilerplate code significantly.

@SpringBootTest
@Testcontainers
class OrderServiceIntegrationTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
        .withDatabaseName("orders_test")
        .withInitScript("schema.sql");

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

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

    @Autowired
    private OrderService orderService;

    @Autowired
    private KafkaTemplate<String, OrderEvent> kafkaTemplate;

    @Test
    void shouldCreateOrderAndPublishEvent() {
        // Arrange
        var request = new CreateOrderRequest("SKU-001", 5, "customer-123");

        // Act
        var order = orderService.createOrder(request);

        // Assert
        assertThat(order.getId()).isNotNull();
        assertThat(order.getStatus()).isEqualTo(OrderStatus.CREATED);

        // Verify Kafka event was published
        var records = KafkaTestUtils.getRecords(consumer, Duration.ofSeconds(10));
        assertThat(records).hasSize(1);
        assertThat(records.iterator().next().value().getOrderId())
            .isEqualTo(order.getId());
    }

    @Test
    void shouldHandleConcurrentOrdersWithOptimisticLocking() {
        var order = orderService.createOrder(
            new CreateOrderRequest("SKU-002", 1, "customer-456")
        );

        // Simulate concurrent updates
        CompletableFuture<Void> update1 = CompletableFuture.runAsync(() ->
            orderService.updateQuantity(order.getId(), 10));
        CompletableFuture<Void> update2 = CompletableFuture.runAsync(() ->
            orderService.updateQuantity(order.getId(), 20));

        // One should succeed, one should throw OptimisticLockException
        assertThatThrownBy(() ->
            CompletableFuture.allOf(update1, update2).join()
        ).hasCauseInstanceOf(OptimisticLockException.class);
    }
}

CI/CD Pipeline Configuration

Testcontainers Cloud integration with GitHub Actions requires installing the TC Cloud agent before running tests. The agent authenticates with your TC Cloud account and routes all Docker API calls to cloud infrastructure. As a result, your CI runners do not need Docker installed at all.

# .github/workflows/integration-tests.yml
name: Integration Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 21
          cache: maven

      - name: Install Testcontainers Cloud Agent
        run: |
          curl -fsSL https://get.testcontainers.cloud/bash | bash
          testcontainers-cloud-agent --wait &
        env:
          TC_CLOUD_TOKEN: ${{ secrets.TC_CLOUD_TOKEN }}

      - name: Run integration tests
        run: ./mvnw verify -Pintegration-tests -Dtest.parallel.threads=4

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: target/surefire-reports/
CI/CD pipeline integration testing with containers
Parallel integration tests running against cloud-hosted containers

Reusable Container Configurations

Therefore, creating a shared test infrastructure module prevents container configuration duplication across test classes. This approach also enables consistent test data setup and teardown patterns.

@TestConfiguration(proxyBeanMethods = false)
public class TestInfrastructureConfig {

    @Bean
    @ServiceConnection
    @RestartScope
    public PostgreSQLContainer<?> postgresContainer() {
        return new PostgreSQLContainer<>("postgres:16-alpine")
            .withDatabaseName("test")
            .withReuse(true)
            .withLabel("reuse.hash", "integration-postgres");
    }

    @Bean
    @ServiceConnection
    @RestartScope
    public KafkaContainer kafkaContainer() {
        return new KafkaContainer(
            DockerImageName.parse("confluentinc/cp-kafka:7.6.0"))
            .withReuse(true)
            .withKraft()
            .withEnv("KAFKA_AUTO_CREATE_TOPICS_ENABLE", "true");
    }

    @Bean
    public WireMockServer paymentServiceMock() {
        var server = new WireMockServer(
            wireMockConfig().dynamicPort());
        server.start();
        return server;
    }
}

// Base test class that all integration tests extend
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Import(TestInfrastructureConfig.class)
@ActiveProfiles("test")
public abstract class BaseIntegrationTest {

    @Autowired
    protected TestRestTemplate restTemplate;

    @Autowired
    protected WireMockServer paymentMock;

    @BeforeEach
    void resetMocks() {
        paymentMock.resetAll();
    }
}

Parallel Test Execution Strategies

Consequently, running tests in parallel dramatically reduces pipeline execution time. Testcontainers Cloud handles the concurrency on the infrastructure side, but your test code needs proper isolation to avoid flaky results.

// JUnit 5 parallel execution configuration
// src/test/resources/junit-platform.properties
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=same_thread
junit.jupiter.execution.parallel.mode.classes.default=concurrent
junit.jupiter.execution.parallel.config.strategy=fixed
junit.jupiter.execution.parallel.config.fixed.parallelism=4

Additionally, each test class should use unique schema prefixes or tenant IDs to prevent data collisions when running concurrently against shared containers.

When NOT to Use Testcontainers Cloud

Testcontainers Cloud adds network latency between your tests and the containers since they run remotely. For tests that make thousands of rapid database calls in tight loops, local containers may actually be faster. Moreover, if your organization has strict data residency requirements, sending test data to cloud infrastructure may not be acceptable.

Unit tests and simple mock-based tests do not benefit from Testcontainers at all. Reserve container-based testing for integration scenarios where you genuinely need a real database, message broker, or external service.

Code review and testing best practices
Choosing the right testing strategy for each layer of your application

Key Takeaways

Testcontainers Cloud integration testing transforms how teams run integration tests in CI/CD pipelines. By offloading container execution to managed infrastructure, you eliminate Docker-in-Docker complexity and reduce pipeline times significantly. Furthermore, Spring Boot’s @ServiceConnection annotation makes configuration nearly zero-effort.

Start by migrating your most resource-intensive integration tests to Testcontainers Cloud and measure the improvement. For further reading, check the Testcontainers Cloud documentation and the Spring Boot Testcontainers reference. You may also find our guide on Spring Boot virtual threads and Java records and sealed classes helpful for building modern Spring applications.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top