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