Production engineering guide for testing microservices covering patterns, implementation strategies, and operational best practices.
Testing Microservices is a systematic approach to ensuring that independently deployable, loosely coupled services communicate correctly, scale reliably, and recover from failures in production. It matters when services are deployed in a dynamic environment with frequent releases, inter-service dependencies, and asynchronous workflows.
Component testing validates the behavior of a single microservice in isolation. It runs quickly and is typically executed during CI, before a service is deployed.
A component test verifies that a service responds correctly to input, processes data, and emits expected outputs. It mocks dependencies like databases, message queues, and external APIs.
go test -v ./internal/service/...
Tests are structured around HTTP endpoints, often using a test server that spins up the service on a random port:
func TestGetUser(t *testing.T) {
server := NewTestServer()
defer server.Close()
client := &http.Client{Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return net.Dial("tcp", server.Listener.Addr().String())
},
}}
resp, err := client.Get("http://localhost:8080/users/1")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("expected status 200, got %d", resp.StatusCode)
}
var user struct{ ID int; Name string }
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
t.Fatal(err)
}
if user.ID != 1 {
t.Errorf("expected user ID 1, got %d", user.ID)
}
}
A service may expose a /health endpoint that returns 200 OK and a JSON body when healthy:
{
"status": "healthy",
"timestamp": "2024-04-18T12:00:00Z",
"dependencies": {
"database": "connected",
"cache": "connected"
}
}
Tests verify this endpoint using curl in a shell script:
curl -f -s http://localhost:8080/health
Failures are silent if the service is slow to start. The health endpoint must be responsive within 100ms; a service that takes 500ms to respond to /health can fail health checks during deployment.
Integration tests verify that multiple services interact correctly under realistic conditions. They require a shared infrastructure: databases, message brokers, and service discovery.
The most common setup uses Docker Compose to orchestrate services and dependencies.
# docker-compose.yml
version: '3.8'
services:
users:
build: ./services/users
ports:
- "8081:8080"
depends_on:
- postgres
- kafka
orders:
build: ./services/orders
ports:
- "8082:8080"
depends_on:
- postgres
- kafka
- users
postgres:
image: postgres:15
environment:
POSTGRES_DB: microservices
POSTGRES_USER: user
POSTGRES_PASSWORD: password
volumes:
- pgdata:/var/lib/postgresql/data
kafka:
image: confluentinc/cp-kafka:7.5
environment:
KAFKA_BROKER_ID: 1
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
volumes:
pgdata:
Integration tests use docker-compose up -d to start the full environment, then wait for services to become ready:
# Wait for users service to be healthy
until curl -f http://localhost:8081/health; do
sleep 1
done
# Wait for Kafka
docker exec kafka kafka-topics.sh --bootstrap-server localhost:9092 --list
Tests then assert end-to-end flows, such as creating a user and placing an order:
# Create a user via the users service
curl -X POST http://localhost:8081/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice"}'
# The orders service should consume the UserCreated event
# Verify the event is in Kafka
docker exec kafka kafka-console-consumer.sh \
--bootstrap-server localhost:9092 \
--topic user-created \
--partition 0 \
--offset 0 \
--max-messages 1
Failure modes include:
Contract testing ensures that service producers and consumers agree on the shape and behavior of data exchanged via APIs or messages.
A contract is a formal agreement: what inputs are expected, what outputs are produced, and how errors are communicated.
The most common contract testing setup uses Pact, a tool that supports both HTTP and message contracts.
A producer service declares its contract:
{
"description": "Given a user ID, return user details",
"provider": "Users Service",
"consumer": "Orders Service",
"state": "user exists",
"request": {
"method": "GET",
"path": "/users/123",
"headers": {
"Accept": "application/json"
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": {
"id": 123,
"name": "Bob",
"email": "[email protected]"
}
}
}
The consumer runs tests against the producer’s contract:
# Run Pact tests using the Pact CLI
pact-provider-verifier \
--provider-base-url http://localhost:8081 \
--pact-uri ./pacts/users-service-orders-service.json \
--provider-states-setup-url http://localhost:8081/setup
The setup endpoint must accept a PUT /_provider-states with a state field:
{
"state": "user exists"
}
The provider must be able to configure its state based on the state string, such as populating the database with a user.
Contract tests fail silently if:
404 for a user that doesn’t exist, but the contract expects 200 with an empty object.End-to-end (E2E) tests simulate real user journeys across multiple services, validating that the system behaves as expected from the outside.
They are slower than component and integration tests but provide confidence that the full stack works together.
E2E tests typically use a test runner like Cucumber or a custom test suite in Go or JavaScript.
Feature: Place an order
Scenario: User adds item to cart and checks out
Given a user "Alice" exists
And a product "Laptop" is available
When Alice adds the Laptop to her cart
And Alice checks out
Then the order is created
And an email is sent to Alice
A test runner executes this scenario by calling services via HTTP, message queues, and event streams.
func TestPlaceOrder(t *testing.T) {
// Start services
dockerComposeUp("e2e")
// Set up initial state
createUser("Alice")
createProduct("Laptop", 999.99)
// Trigger order flow
addProductToCart("Alice", "Laptop")
checkout("Alice")
// Verify outcomes
order, err := getOrder("Alice", "Laptop")
if err != nil {
t.Fatal(err)
}
if order.Status != "confirmed" {
t.Errorf("expected order status 'confirmed', got %s", order.Status)
}
email := getEmailFor("Alice")
if !strings.Contains(email.Body, "Thank you for your order") {
t.Errorf("email body missing confirmation text")
}
}
E2E tests often use a test database that is reset before each run:
# Reset the test database
psql -U user -d microservices -c "TRUNCATE TABLE users, orders, carts RESTART IDENTITY;"
Key failure modes:
201 Created but the Location header is missing.Chaos testing validates that the system recovers from failures in production, such as network latency, service crashes, and database outages.
It is executed periodically, often in a staging environment, and can be triggered by CI or manually.
A common chaos experiment uses a script that introduces latency, crashes services, and kills network connections.
#!/bin/bash
# chaos-test.sh
# Introduce 500ms latency to all services
sudo tc qdisc add dev eth0 root handle 1: htb default 10
sudo tc class add dev eth0 parent 1: classid 1:1 htb rate 100mbit
sudo tc qdisc add dev eth0 parent 1:1 handle 10: netem delay 500ms
# Simulate database failure
docker exec db psql -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid <> pg_backend_pid() AND now() - query_start > interval '10 minutes'"
# Kill users service
pkill -f users-service
# Wait 60 seconds
sleep 60
# Check health of services
curl -f http://localhost:8081/health
curl -f http://localhost:8082/health
# Clean up
sudo tc qdisc del dev eth0 root
The test is considered successful if:
Silent failures include:
500 error but doesn’t emit it to the monitoring system.Tests are orchestrated using CI/CD pipelines. Each stage runs a subset of tests based on the change.
A typical pipeline uses GitHub Actions or GitLab CI.
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
component:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: go test -v ./internal/...
integration:
needs: component
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker-compose up -d --build
- run: |
until curl -f http://localhost:8081/health; do sleep 1; done
curl -X POST http://localhost:8081/users -d '{"name": "Test User"}'
# Add integration test commands
contract:
needs: component
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pact publish --pact-broker-base-url https://pact-broker.example.com
e2e:
needs: integration
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker-compose up -d --build
- run: go test -v ./e2e/
The pipeline fails if:
Testing microservices is not about running tests. It is about ensuring that services speak the same language, survive the unexpected, and deliver correct results under pressure.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy