How to Use Testcontainers for Microservices Testing
Use
Testcontainers to spin up real Docker containers for each microservice during tests, ensuring isolated and realistic environments. Define containers in your test code, configure networking between them, and run integration tests against these live services.Syntax
Testcontainers uses container classes to define and manage Docker containers in tests. You create containers for each microservice, configure ports and environment variables, and start them before tests run.
GenericContainer: Base container class for any Docker image.withExposedPorts(): Opens ports to communicate with the container.withEnv(): Sets environment variables inside the container.start(): Starts the container before tests.stop(): Stops the container after tests.
java
GenericContainer<?> serviceA = new GenericContainer<>("service-a-image:latest") .withExposedPorts(8080) .withEnv("CONFIG", "value"); GenericContainer<?> serviceB = new GenericContainer<>("service-b-image:latest") .withExposedPorts(9090) .withEnv("DEPENDENCY_URL", "http://service-a:8080"); serviceA.start(); serviceB.start(); // Run tests here serviceB.stop(); serviceA.stop();
Example
This example shows how to use Testcontainers to start two microservices containers and test their interaction. It demonstrates container setup, networking, and a simple HTTP request to verify service communication.
java
import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.DockerImageName; import java.net.HttpURLConnection; import java.net.URL; import static org.junit.jupiter.api.Assertions.assertEquals; public class MicroservicesTest { static GenericContainer<?> serviceA = new GenericContainer<>(DockerImageName.parse("service-a-image:latest")) .withExposedPorts(8080); static GenericContainer<?> serviceB = new GenericContainer<>(DockerImageName.parse("service-b-image:latest")) .withExposedPorts(9090); @BeforeAll public static void setUp() { serviceA.start(); serviceB.withEnv("SERVICE_A_URL", "http://" + serviceA.getHost() + ":" + serviceA.getFirstMappedPort()); serviceB.start(); } @AfterAll public static void tearDown() { serviceB.stop(); serviceA.stop(); } @Test public void testServiceBCallsServiceA() throws Exception { String serviceBUrl = "http://" + serviceB.getHost() + ":" + serviceB.getFirstMappedPort() + "/call-service-a"; HttpURLConnection connection = (HttpURLConnection) new URL(serviceBUrl).openConnection(); connection.setRequestMethod("GET"); int responseCode = connection.getResponseCode(); assertEquals(200, responseCode); } }
Output
Tests run successfully with HTTP 200 response confirming service interaction.
Common Pitfalls
- Not waiting for containers to be ready: Containers may start but services inside might not be ready immediately. Use health checks or wait strategies.
- Incorrect port mapping: Forgetting to expose and map ports causes connection failures.
- Hardcoding container hostnames: Use dynamic host and port from Testcontainers, not fixed values.
- Not stopping containers: Can cause resource leaks and test interference.
java
/* Wrong: Hardcoded port and hostname */ String url = "http://localhost:8080/api"; /* Right: Use Testcontainers dynamic host and port */ String url = "http://" + serviceA.getHost() + ":" + serviceA.getFirstMappedPort() + "/api";
Quick Reference
| Feature | Description | Usage |
|---|---|---|
| GenericContainer | Base class to create any Docker container | new GenericContainer<>("image:tag") |
| withExposedPorts | Expose ports to communicate with container | .withExposedPorts(8080) |
| withEnv | Set environment variables inside container | .withEnv("KEY", "value") |
| start | Start container before tests | .start() |
| stop | Stop container after tests | .stop() |
| getHost & getFirstMappedPort | Get container host and mapped port | container.getHost(), container.getFirstMappedPort() |
| Wait strategies | Wait for container readiness | withStartupTimeout(), waitingFor() |
Key Takeaways
Use Testcontainers to run real Docker containers for each microservice during integration tests.
Always expose and map ports correctly and use dynamic host and port from Testcontainers.
Implement wait strategies to ensure services inside containers are ready before testing.
Stop containers after tests to free resources and avoid conflicts.
Configure environment variables to simulate real microservice dependencies.