Modern software architectures are dominated by microservices, each with its own database, message broker, or third-party integration. Testing them in isolation is not enough — developers must validate real-world interactions between services. This is where Testcontainers with Docker Compose shines, enabling reliable integration and end-to-end testing in disposable environments.
In this tutorial, you will learn how to set up and test microservices using Testcontainers and Docker Compose, ensuring your services run exactly as they would in production.
What Are Testcontainers and Docker Compose?
- Testcontainers: A Java testing library that uses Docker containers for integration testing. It lets you run databases, Kafka, RabbitMQ, or even full microservices inside tests.
- Docker Compose: A tool for defining and managing multi-container applications, often used to orchestrate entire microservice stacks.
By combining both, you can spin up multiple services at once (databases, APIs, brokers) and run real integration tests without complex manual setup.
Why Test Microservices with Testcontainers + Docker Compose?
- ✅ Realistic Testing: Services interact with real containers instead of mocks.
- ✅ Reproducibility: Same stack works across dev, CI/CD, and staging.
- ✅ Isolation: Each test suite runs in its own environment, preventing conflicts.
- ✅ Automation: Perfect for CI/CD pipelines (GitHub Actions, Jenkins, GitLab CI).
Setting Up Dependencies
Add the following to your Maven pom.xml
:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>docker-compose</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
For Gradle:
testImplementation "org.testcontainers:junit-jupiter:1.19.3"
testImplementation "org.testcontainers:docker-compose:1.19.3"
Defining a Docker Compose File
Create a docker-compose.yml
file in src/test/resources
:
version: '3.8'
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- "5432:5432"
kafka:
image: confluentinc/cp-kafka:7.4.0
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
ports:
- "9092:9092"
zookeeper:
image: confluentinc/cp-zookeeper:7.4.0
environment:
ZOOKEEPER_CLIENT_PORT: 2181
This stack runs PostgreSQL, Kafka, and Zookeeper for our microservices to use.
Writing a JUnit 5 Test with Docker Compose
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.DockerComposeContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.io.File;
@Testcontainers
public class MicroserviceIntegrationTest {
@Container
public static DockerComposeContainer<?> environment =
new DockerComposeContainer<>(new File("src/test/resources/docker-compose.yml"))
.withExposedService("postgres_1", 5432)
.withExposedService("kafka_1", 9092);
@Test
void testMicroserviceCommunication() {
String dbHost = environment.getServiceHost("postgres_1", 5432);
Integer dbPort = environment.getServicePort("postgres_1", 5432);
System.out.println("Postgres running at " + dbHost + ":" + dbPort);
String kafkaHost = environment.getServiceHost("kafka_1", 9092);
Integer kafkaPort = environment.getServicePort("kafka_1", 9092);
System.out.println("Kafka running at " + kafkaHost + ":" + kafkaPort);
// Here you can initialize your microservices, connect them to DB + Kafka, and run assertions
}
}
Sample Output
Postgres running at localhost:32784
Kafka running at localhost:32785
Advanced Use Cases
1. Running Multiple Microservices
You can extend your Compose file with Spring Boot services and verify real inter-service communication.
2. CI/CD Integration
In Jenkins or GitHub Actions, Testcontainers ensures tests run in ephemeral environments identical to production.
3. Flaky Test Mitigation
Containers start fresh for every run, preventing dependency conflicts.
Best Practices
- Keep Docker images lightweight (use
alpine
variants). - Pin exact image versions to avoid breaking changes.
- Use
@Testcontainers
with@Container
for automatic lifecycle management. - Reuse containers in long test suites for speed improvements.
- Log container output for debugging failed tests.
Version Tracker
- JUnit 4 → JUnit 5: Native support for
@Testcontainers
and@Container
. - Mockito Updates: Now supports mocking static/final methods without PowerMock.
- Testcontainers Growth: New modules (Kafka, LocalStack, Docker Compose) added for microservices.
Conclusion
Testing microservices is challenging, but Testcontainers + Docker Compose simplifies integration testing by spinning up production-like environments inside your tests. With this approach, you can validate databases, message brokers, and service communication reliably — all within your CI/CD pipeline.
Key Takeaways
- Testcontainers + Docker Compose provide realistic microservice testing environments.
- Perfect for databases, brokers, and multi-service stacks.
- Works seamlessly with JUnit 5, Mockito, and Spring Boot.
- Boosts confidence in microservice deployments by catching integration issues early.
FAQ
Q1. What’s the difference between unit and integration tests?
Unit tests isolate logic, while integration tests validate multiple components/services working together.
Q2. Can I mock a static method in Mockito?
Yes, Mockito (3.4+) supports mocking static methods directly.
Q3. How does Testcontainers help in CI/CD pipelines?
It ensures services run in containers identical to production, reducing environment drift.
Q4. Can I test microservices without Docker Compose?
Yes, but Compose simplifies orchestration for multi-container setups.
Q5. How do I handle flaky container startup?
Use .waitingFor()
conditions in Testcontainers to wait for health checks.
Q6. Can I run Testcontainers with Kubernetes?
Yes, with integrations like LocalStack or Kind, though Docker Compose is simpler for local dev.
Q7. How do I test message-driven microservices?
Leverage Testcontainers Kafka or RabbitMQ modules to simulate real message brokers.
Q8. What’s the difference between TDD and BDD?
TDD focuses on implementation-driven tests, while BDD emphasizes behavior and collaboration.
Q9. How can I debug failing Testcontainers tests?
Enable container logs with followOutput
or print mapped ports for clarity.
Q10. Is Testcontainers production-ready?
It’s a testing-only library, not for production, but perfect for CI/CD pipelines.