Contract testing with WireMock


Have you ever come across a codebase with thousands of unit and integration tests, and still lacked confidence that what you pushed to production would not break a happy path or introduce a regression?

Modern applications consume many third-party APIs. Unit tests and integration tests that rely on hand-rolled mocks often are not enough. WireMock lets you stub third-party HTTP request/response pairs with your own datasets and fixtures. Build those fixtures from real application traffic—pulled from monitoring tools or your database—so you exercise production-like paths and edge cases and catch regressions earlier.

These tests move you closer to confident deploys because they run real production code paths end to end inside your service, not just isolated methods, using examples taken from logs and monitoring.

WireMock is not a replacement for fast unit tests that stub a client method in-process, and it is not a full end-to-end test against live production providers. Use it when you want to exercise real production paths against your codebase in isolation.

Key benefits

  • Complete isolation: Run tests locally or in CI without depending on flaky or unreleased third-party systems.
  • Fault and error simulation: Force HTTP error codes (400, 404, 500) and network timeouts to validate resilience.
  • Request verification: Capture incoming requests so you can assert that your app sent the correct parameters, headers, or payloads.
  • Stateful behavior: Mock multi-step workflows (e.g. “pending” on the first call, “complete” on the second).
  • Record and playback: Capture traffic from real APIs to generate reusable stubs.

Sample stack

  • WireMock — HTTP stubbing at the wire level
  • Docker — same WireMock process in local runs and CI
  • pytest — fixtures to check the container and point the app at it

Why this combo

  • Stub path, method, headers, and body without touching production APIs
  • Replay recorded traffic or load fixtures from production-like JSON
  • Keep local and CI environments aligned via a container image
  • Keep tests readable: pytest owns lifecycle; WireMock owns HTTP behavior

Project tree (docker-compose)

Keep WireMock stubs next to the app and mount them into the container. Know this layout before you scaffold files:

.
├── .github/
│   └── workflows/
│       └── test.yml            # start WireMock → pytest → tear down
├── docker-compose.yml          # WireMock service + volume mounts
├── requirements-dev.txt        # or pyproject.toml — pytest, requests, …
├── pytest.ini
├── src/
│   └── myapp/                  # your Python service / client under test
│       └── clients/
│           └── orders.py       # talks to ORDERS_API_BASE_URL
├── tests/
│   ├── conftest.py             # pytest fixtures (WireMock URL, app client)
│   └── test_orders.py          # integration tests against stubs
└── wiremock/                   # everything WireMock loads at startup
    ├── mappings/               # request → response rules (JSON)
    │   ├── get-order-123.json
    │   └── get-order-404.json
    └── __files/                # response bodies referenced by mappings
        └── orders/
            ├── order-123.json  # happy-path payload (real-world sample)
            └── order-missing.json
Path Role
docker-compose.yml Starts WireMock and mounts wiremock/ into the container
wiremock/mappings/ Stub definitions (method, URL, matchers, status, which body file)
wiremock/__files/ HTTP response bodies; bodyFileName is relative to this folder
tests/conftest.py Points the app at http://localhost:8080 (or compose DNS in CI)
src/myapp/ Real code under test — no knowledge of WireMock, only the base URL

WireMock’s container paths are fixed: host ./wiremock/mappings/home/wiremock/mappings, and ./wiremock/__files/home/wiremock/__files.

Setup guide: from scratch

End-to-end path to get your first WireMock + pytest test green. Do these steps in order.

1. Install prerequisites

Tool Why Check
Python 3.11+ App + pytest python3 --version
Docker Desktop (or Engine + Compose plugin) Runs the WireMock container docker --version and docker compose version
Git (optional) Version stubs with the app git --version

You do not install a WireMock JAR or Java locally — the official image provides WireMock inside Docker.

2. Create a virtualenv and install test deps

python3 -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate

pip install pytest requests
# optional but common: httpx, pytest-dotenv

Minimal requirements-dev.txt:

pytest>=8.0
requests>=2.31

3. Scaffold directories

mkdir -p src/myapp/clients tests wiremock/mappings wiremock/__files/orders

Point your HTTP client at a configurable base URL (e.g. ORDERS_API_BASE_URL). Tests will override it to WireMock; production keeps the real host.

4. Add docker-compose and WireMock stubs

Create docker-compose.yml at the repo root:

services:
  wiremock:
    image: wiremock/wiremock:3.9.1
    ports:
      - "8080:8080"
    volumes:
      - ./wiremock/mappings:/home/wiremock/mappings
      - ./wiremock/__files:/home/wiremock/__files
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/__admin/mappings"]
      interval: 2s
      timeout: 2s
      retries: 15
      start_period: 5s

Add a mapping — wiremock/mappings/get-order-123.json:

{
  "request": {
    "method": "GET",
    "urlPath": "/v1/orders/123"
  },
  "response": {
    "status": 200,
    "headers": { "Content-Type": "application/json" },
    "bodyFileName": "orders/order-123.json"
  }
}

Add the body file it references — wiremock/__files/orders/order-123.json (replace with a sanitized production sample):

{
  "id": "123",
  "status": "SHIPPED",
  "items": [{ "sku": "SKU-9", "qty": 2 }],
  "shipping": { "carrier": "UPS", "eta": "2026-08-05" }
}

bodyFileName is resolved relative to wiremock/__files/.

5. Configure pytest

Create pytest.ini (or [tool.pytest.ini_options] in pyproject.toml):

[pytest]
testpaths = tests
pythonpath = src

pythonpath = src lets tests import myapp without packaging tricks.

Create tests/conftest.py:

import os
import pytest
import requests

WIREMOCK_URL = os.getenv("WIREMOCK_URL", "http://localhost:8080")


@pytest.fixture(scope="session")
def wiremock_url():
    # Fail fast if `docker compose up` was not run
    resp = requests.get(f"{WIREMOCK_URL}/__admin/mappings", timeout=2)
    resp.raise_for_status()
    yield WIREMOCK_URL


@pytest.fixture
def app_client(wiremock_url, monkeypatch):
    monkeypatch.setenv("ORDERS_API_BASE_URL", wiremock_url)
    # TODO: return your app's test client / httpx client configured for tests
    ...

Create tests/test_orders.py:

def test_get_order_uses_stubbed_provider(app_client):
    # TODO: call your service; assert it handled the WireMock response
    ...

6. Pull the image and start WireMock

docker compose pull wiremock
docker compose up -d wiremock

# confirm admin API is up and mappings loaded
curl -s http://localhost:8080/__admin/mappings | head
curl -s http://localhost:8080/v1/orders/123

If the second curl returns your fixture JSON, stubs are wired correctly. After editing files under wiremock/, restart so WireMock reloads them:

docker compose restart wiremock

7. Run the tests

# WireMock must already be running (step 6)
pytest -q

# one file / one test
pytest tests/test_orders.py -v
pytest tests/test_orders.py::test_get_order_uses_stubbed_provider -v

Override the URL when Compose is not publishing to localhost (e.g. a CI sibling network):

WIREMOCK_URL=http://wiremock:8080 pytest -q

8. Day-to-day loop

docker compose up -d wiremock   # once per session
# edit mappings / __files / tests
docker compose restart wiremock # after stub file changes
pytest -q
docker compose down             # when you are done

CI with GitHub Actions

Reuse the same docker-compose.yml on the runner: start WireMock, wait until the admin API is healthy, run pytest against localhost:8080, then tear down. Pin the WireMock image tag so CI and laptops stay aligned.

Workflow scaffold

.github/workflows/test.yml:

name: Test

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          cache: pip

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements-dev.txt
          # or: pip install -e ".[dev]"

      - name: Start WireMock
        run: docker compose up -d --wait wiremock

      - name: Run tests
        env:
          WIREMOCK_URL: http://localhost:8080
        run: pytest -q

      - name: Stop WireMock
        if: always()
        run: docker compose down

GitHub-hosted runners already have Docker and Compose. Port mapping 8080:8080 means pytest on the runner can use http://localhost:8080—the same default as local. The healthcheck in compose (step 4) makes --wait reliable; if you skip the healthcheck, keep an explicit curl wait loop before pytest.

CI checklist

TODO:
- [ ] Commit docker-compose.yml + wiremock/ mappings and __files
- [ ] Add .github/workflows/test.yml
- [ ] Pin wiremock/wiremock:<version> (avoid :latest)
- [ ] Fail the job on stub load errors (check compose logs if pytest fails oddly)
- [ ] Cache pip; keep Python version in sync with local/dev

Next topics you can explore with WireMock

  • How you sanitize PII from real responses before committing fixtures
  • Admin API: reset mappings between tests when stubs are dynamic
  • A short before/after of a flaky integration test
  • Recording mode vs pure stub mode; Testcontainers instead of compose