Skip to content

Matrix Testing

Learn how to test multiple services in parallel using GitHub Actions matrix strategy with isolated Docker environments.

Overview

Matrix testing allows you to run HTTPTests for multiple services simultaneously, each in its own isolated environment, providing faster feedback and better parallelization.

The Problem

When you have multiple services to test:

your-repo/
├── service-a/.httptests/
├── service-b/.httptests/
├── service-c/.httptests/
├── service-d/.httptests/
└── service-e/.httptests/

Sequential approach:

yaml
# ❌ Slow - runs one after another
- uses: serviceguards-com/httptests-action@v2
  with:
    httptests-directory: ./service-a
- uses: serviceguards-com/httptests-action@v2
  with:
    httptests-directory: ./service-b
# ... and so on

Problems:

  • ❌ Slow - tests run sequentially
  • ❌ Long feedback cycles
  • ❌ One failure blocks others
  • ❌ Poor resource utilization

The Solution: Matrix Strategy

Run all services in parallel with GitHub Actions matrix strategy:

yaml
name: HTTPTests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        service:
          - service-a
          - service-b
          - service-c
          - service-d
          - service-e
    steps:
      - uses: actions/checkout@v4
      - uses: serviceguards-com/httptests-action@v2
        with:
          httptests-directory: ./${{ matrix.service }}

Benefits:

  • ✅ Tests run in parallel
  • ✅ Faster overall execution
  • ✅ Independent failure isolation
  • ✅ Better GitHub Actions UI
  • ✅ Isolated Docker environments

Basic Matrix Configuration

Simple Service List

yaml
name: HTTPTests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        test-suite:
          - ./api-gateway
          - ./user-service
          - ./order-service
          - ./payment-service
    steps:
      - uses: actions/checkout@v4
      - uses: serviceguards-com/httptests-action@v2
        with:
          httptests-directory: ${{ matrix.test-suite }}

With Job Names

yaml
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        service:
          - name: API Gateway
            path: ./api-gateway
          - name: User Service
            path: ./user-service
          - name: Order Service
            path: ./order-service
    
    name: Test ${{ matrix.service.name }}
    
    steps:
      - uses: actions/checkout@v4
      - uses: serviceguards-com/httptests-action@v2
        with:
          httptests-directory: ${{ matrix.service.path }}

Advanced Configurations

Fail-Fast vs Continue on Error

Fail-Fast (Default)

yaml
strategy:
  matrix:
    service: [service-a, service-b, service-c]
  # fail-fast: true (default)

Behavior: If one service fails, all others are cancelled.

Use when: You want quick feedback and failing fast saves time.

Continue on Error

yaml
strategy:
  matrix:
    service: [service-a, service-b, service-c]
  fail-fast: false

Behavior: All services continue testing even if one fails.

Use when: You want to see all failures, not just the first one.

Timeout Configuration

yaml
jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 10  # Per service timeout
    strategy:
      matrix:
        service: [service-a, service-b, service-c]
    steps:
      - uses: actions/checkout@v4
      - uses: serviceguards-com/httptests-action@v2
        with:
          httptests-directory: ./${{ matrix.service }}

Conditional Matrix

Run only changed services:

yaml
jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      services: ${{ steps.filter.outputs.changes }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v2
        id: filter
        with:
          filters: |
            service-a:
              - 'services/service-a/**'
            service-b:
              - 'services/service-b/**'
            service-c:
              - 'services/service-c/**'

  test:
    needs: detect-changes
    if: ${{ needs.detect-changes.outputs.services != '[]' }}
    runs-on: ubuntu-latest
    strategy:
      matrix:
        service: ${{ fromJSON(needs.detect-changes.outputs.services) }}
    steps:
      - uses: actions/checkout@v4
      - uses: serviceguards-com/httptests-action@v2
        with:
          httptests-directory: ./services/${{ matrix.service }}

Complete Examples

Example 1: Microservices Monorepo

yaml
name: Microservices Tests
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    strategy:
      fail-fast: false
      matrix:
        service:
          - api-gateway
          - user-service
          - order-service
          - payment-service
          - notification-service
    
    name: Test ${{ matrix.service }}
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Run HTTPTests
        uses: serviceguards-com/httptests-action@v2
        with:
          httptests-directory: ./services/${{ matrix.service }}
          python-version: '3.11'

Example 2: Different Service Categories

yaml
name: HTTPTests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        include:
          - category: "API"
            service: api-gateway
            timeout: 10
          - category: "API"
            service: rest-api
            timeout: 10
          - category: "Proxy"
            service: nginx-proxy
            timeout: 5
          - category: "Proxy"
            service: load-balancer
            timeout: 5
    
    name: Test ${{ matrix.category }} - ${{ matrix.service }}
    timeout-minutes: ${{ matrix.timeout }}
    
    steps:
      - uses: actions/checkout@v4
      - uses: serviceguards-com/httptests-action@v2
        with:
          httptests-directory: ./${{ matrix.service }}

Example 3: Environment-Specific Testing

yaml
name: Multi-Environment Tests
on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        service: [api-gateway, user-service]
        environment: [staging, production]
    
    name: Test ${{ matrix.service }} (${{ matrix.environment }})
    
    steps:
      - uses: actions/checkout@v4
      - uses: serviceguards-com/httptests-action@v2
        with:
          httptests-directory: ./${{ matrix.service }}-${{ matrix.environment }}

Isolated Docker Environments

Each matrix job runs in its own runner with isolated Docker:

yaml
# Job 1: service-a
🚀 Starting environment...
docker compose -p httptests-service-a up -d

# Job 2: service-b (parallel)
🚀 Starting environment...
docker compose -p httptests-service-b up -d

# Job 3: service-c (parallel)
🚀 Starting environment...
docker compose -p httptests-service-c up -d

Key features:

  • ✅ Separate Docker networks per service
  • ✅ No port conflicts
  • ✅ Independent container lifecycle
  • ✅ Automatic cleanup per job

GitHub Actions UI Benefits

Matrix testing provides clear UI visualization:

✅ test (api-gateway)        - Passed in 2m 34s
✅ test (user-service)       - Passed in 1m 52s
❌ test (order-service)      - Failed in 3m 12s
✅ test (payment-service)    - Passed in 2m 03s
✅ test (notification-service) - Passed in 1m 28s

Benefits:

  • See status of each service independently
  • Quick identification of which service failed
  • Individual logs per service
  • Re-run only failed jobs

Best Practices

1. Use Descriptive Job Names

yaml
# Good
strategy:
  matrix:
    service:
      - name: API Gateway
        path: ./api-gateway
      - name: User Service
        path: ./user-service

name: Test ${{ matrix.service.name }}

# Bad
strategy:
  matrix:
    service: [./api-gateway, ./user-service]
name: Test ${{ matrix.service }}  # Shows "./api-gateway"

2. Set Appropriate Timeouts

yaml
jobs:
  test:
    timeout-minutes: 15  # Prevent hanging jobs
    strategy:
      matrix:
        service: [...]

3. Use fail-fast Appropriately

yaml
# Development - fail fast to save time
strategy:
  matrix:
    service: [...]
  fail-fast: true

# CI/CD - see all failures
strategy:
  matrix:
    service: [...]
  fail-fast: false

4. Limit Concurrency for Resource Constraints

yaml
strategy:
  max-parallel: 3  # Run max 3 jobs at once
  matrix:
    service: [s1, s2, s3, s4, s5, s6]
yaml
# Separate workflows for different layers
name: API Layer Tests
strategy:
  matrix:
    service: [api-gateway, rest-api, graphql-api]

---

name: Service Layer Tests
strategy:
  matrix:
    service: [user-service, order-service, payment-service]

Comparison: Matrix vs Separate Jobs

yaml
jobs:
  test:
    strategy:
      matrix:
        service: [a, b, c]

Pros:

  • ✅ Less code duplication
  • ✅ Easier to add/remove services
  • ✅ Better GitHub UI
  • ✅ Consistent configuration

Cons:

  • ⚠️ All jobs use same configuration
  • ⚠️ Harder to customize per service

Separate Jobs

yaml
jobs:
  test-a:
    steps: [...]
  test-b:
    steps: [...]
  test-c:
    steps: [...]

Pros:

  • ✅ Full control per job
  • ✅ Easy to customize each service
  • ✅ Can have dependencies between jobs

Cons:

  • ❌ More code duplication
  • ❌ Harder to maintain
  • ❌ More verbose

Troubleshooting

Tests Run Sequentially Instead of Parallel

Issue: Jobs appear to run one after another

Cause: GitHub Actions concurrent job limits

Solutions:

  1. Check your GitHub plan's concurrency limits
  2. Use max-parallel to adjust
  3. Check organization/repository settings

Resource Exhaustion

Issue: Jobs fail with "Docker daemon not responding"

Cause: Too many parallel Docker operations

Solutions:

yaml
strategy:
  max-parallel: 5  # Limit concurrent jobs
  matrix:
    service: [...]

Port Conflicts

Issue: "Port already in use" errors

Cause: Matrix jobs sharing ports (shouldn't happen)

Solutions:

  • Each job gets isolated runner - this shouldn't occur
  • If it does, check for host port mappings in docker-compose
  • HTTPTests uses isolated project names automatically

Performance Tips

1. Optimize Docker Builds

dockerfile
# Use specific base image versions
FROM nginx:1.25-alpine

# Minimize layers
COPY nginx.conf /etc/nginx/nginx.conf

# No unnecessary packages

2. Cache Dependencies

yaml
- name: Cache Python dependencies
  uses: actions/cache@v3
  with:
    path: ~/.cache/pip
    key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}

3. Parallelize Appropriately

yaml
# If you have 20 services, don't run all at once
strategy:
  max-parallel: 10  # Balance speed vs resources

Released under the MIT License.