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.
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/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=4Additionally, 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.
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.