Testing Microservices

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

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 Testing with Shared Infrastructure

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

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:

End-to-End Testing

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:

Chaos Testing

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:

Test Orchestration and Execution

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:

Shared Patterns and Pitfalls

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.