Mock Servers & Contract Testing from OpenAPI
This guide is part of OpenAPI & AsyncAPI Schema Authoring and covers the two things a specification can do beyond documenting an API: stand in for the implementation while it is being built, and act as the oracle that proves the implementation is correct. It walks through running a mock server from a spec, generating property-based tests from the same document, validating live responses in CI, and the discipline that keeps all three honest. It does not cover consumer-driven contract testing in the Pact sense, where consumers publish their own expectations — that is a different model with a different registry.
The reason this matters is drift. A specification that nothing verifies is a document about an API rather than a description of one, and the gap opens quietly: a field renamed in a hurry, a status code changed to fix a client bug, an optional field that became required. Nobody notices because the only thing checking the specification is a renderer, and a renderer will happily draw a beautiful page describing behaviour that no longer exists. Contract testing closes that gap by making the specification executable, which turns every deviation into a failing build instead of a support ticket six weeks later.
Quick reference: tool selection
| Tool | What it does | Needs a running API | Finds |
|---|---|---|---|
Prism (@stoplight/prism-cli) |
Mock server from a spec; also a validating proxy | No, for mocking | Consumer assumptions that the contract does not support |
| Schemathesis | Generates property-based tests from the spec | Yes | Crashes, undocumented status codes, schema violations |
| Dredd | Replays documented examples against the API | Yes | Examples that no longer match reality |
openapi-backend / middleware validators |
Validates live requests and responses at runtime | Yes | Drift in production traffic, continuously |
oasdiff |
Diffs two spec versions | No | Breaking changes before release |
These are complementary rather than alternatives. Prism unblocks the consumer, Schemathesis interrogates the producer, a runtime validator watches production, and oasdiff guards the contract itself. A reasonable minimum is Prism plus one producer-side check; adding a runtime validator in staging is the highest-value next step because it observes traffic shapes no test author thought to write.
The distinction that governs the whole design is direction. A mock answers “does my client match the contract?” — it is a producer stand-in and it can never be wrong, because it is the contract. A contract test answers “does the producer match the contract?” — it is a consumer stand-in and its whole value is that it can fail. Teams that only mock end up with clients that match a document and a server that does not, which is the most expensive way to be wrong, because everything passes right up until integration.
Prerequisites & Environment Setup
- A bundled OpenAPI 3.x document. Multi-file specs must be resolved first; both Prism and Schemathesis read one document.
- Node 18+ for Prism, Python 3.9+ for Schemathesis. They are independent tools and do not need to share a runtime.
- Examples in the spec. Mock quality is entirely determined by this, and it is the single highest-leverage investment in this guide.
- A deployable test instance for the producer-side checks, with data you can safely mutate.
# Mocking
npm install --save-dev @stoplight/[email protected]
# Producer-side property testing
python -m pip install 'schemathesis==3.39.5'
# Bundle first — both tools want a single resolved document
npx @redocly/cli@2 bundle openapi/api.yaml -o dist/api.bundled.yaml
Verify the bundle before either tool touches it. A spec that fails to resolve produces confusing downstream errors that look like tool bugs:
npx @redocly/cli@2 lint dist/api.bundled.yaml
Woohoo! Your API description is valid. 🎉
Core Configuration
Start the mock. Prism reads the document and serves every documented operation immediately, with no configuration file at all:
npx prism mock dist/api.bundled.yaml --port 4010 --host 0.0.0.0
[CLI] … awaiting Starting Prism…
[CLI] ℹ info GET http://0.0.0.0:4010/invoices
[CLI] ℹ info POST http://0.0.0.0:4010/invoices
[CLI] ℹ info GET http://0.0.0.0:4010/invoices/{id}
[CLI] ▶ start Prism is listening on http://0.0.0.0:4010
By default Prism returns the first example it finds, falling back to generating a value from the schema. That fallback is why mock quality tracks example quality so directly — an operation with no example returns "string" and 0, which is type-correct and useless for building a UI against. Put realistic examples in the spec and the mock becomes a genuinely useful development target:
paths:
/invoices/{id}:
get:
operationId: getInvoice
responses:
'200':
description: The invoice
content:
application/json:
schema: { $ref: '#/components/schemas/Invoice' }
examples:
paid:
summary: A settled invoice
value:
id: inv_01H8XK
status: paid
total_cents: 24900
currency: EUR
issued_at: '2026-07-01T09:12:00Z'
overdue:
summary: Past its due date
value:
id: inv_01H8XM
status: overdue
total_cents: 24900
currency: EUR
issued_at: '2026-05-01T09:12:00Z'
'404':
description: No such invoice
content:
application/problem+json:
example:
type: https://acme.com/problems/not-found
title: Invoice not found
status: 404
Consumers select a named example with a header, which is what makes a mock useful for testing branches rather than only the happy path:
curl -s http://localhost:4010/invoices/inv_01H8XM \
-H 'Prefer: example=overdue' | jq -r .status
overdue
# Force a specific status code to exercise the client's error handling
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:4010/invoices/nope \
-H 'Prefer: code=404'
404
That Prefer header is the feature that separates a mock you can build a real client against from one that only demonstrates the happy path. Error handling is where client bugs live, and a mock that can be asked for a 404, a 409, or a 500 on demand lets a consumer test every branch before the API exists.
Integration Pattern
Two jobs, on different triggers. The mock runs alongside consumer test suites; the contract tests run against a deployed instance after every producer change.
# .github/workflows/contract.yml
name: Contract
on:
push:
branches: [main]
paths: ['openapi/**', 'src/**']
pull_request:
paths: ['openapi/**', 'src/**']
jobs:
consumer-against-mock:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- run: npx @redocly/cli@2 bundle openapi/api.yaml -o dist/api.bundled.yaml
# Start the mock in the background and wait for it to accept connections.
- name: Start mock
run: |
npx prism mock dist/api.bundled.yaml --port 4010 &
npx wait-on http://127.0.0.1:4010/invoices --timeout 30000
# The client's own tests, pointed at the mock instead of a real API.
- name: Run consumer tests
run: npm test
env:
API_BASE_URL: http://127.0.0.1:4010
producer-against-spec:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.12' }
- run: python -m pip install 'schemathesis==3.39.5'
# Property-based testing: generates inputs from the schema and asserts
# every response conforms to what the spec documents.
- name: Contract test the deployed API
run: |
schemathesis run dist/api.bundled.yaml \
--base-url "${{ vars.STAGING_BASE_URL }}" \
--checks all \
--hypothesis-max-examples 40 \
--header "Authorization: Bearer ${{ secrets.STAGING_TOKEN }}" \
--report
--checks all is the setting that earns the tool its place. It asserts not only that responses match their declared schema, but that the status code is documented at all, that the content type matches, that a server never returns a 500 for a well-formed request, and that headers the spec declares are actually present. Most first runs fail on undocumented status codes, which is exactly the drift this is meant to surface.
Use Prism’s proxy mode to get continuous validation without writing any tests. It forwards traffic to the real API and flags every response that violates the contract, which makes it a useful thing to put in front of a staging environment:
npx prism proxy dist/api.bundled.yaml https://staging.api.acme.com --errors
With --errors, a non-conforming response becomes a 500 with a violation report, so a drifting endpoint fails loudly in staging rather than silently in production.
Advanced Options
Make the portal console mock-backed for risky operations. A destructive or unreleased endpoint should not be callable against a real environment from a documentation page. Pointing the try-it console at a mock keeps the demo useful and the blast radius zero, and because the mock is generated it never falls out of date. See Interactive API Consoles & Try-It Panels for wiring the console to an alternate server.
Seed generation with realistic constraints. Property-based testing finds bugs in proportion to how well the schema constrains its inputs. A type: string field generates arbitrary Unicode; a field with format, pattern, minLength, and maxLength generates values that look like real ones and exercise real code paths. Tightening the schema improves both the tests and the documentation at once.
Record and replay production shapes. Sampling real traffic and validating it against the spec finds the drift no test anticipated, because production sends combinations nobody wrote a case for. A sampling validator in staging costs little and reports on the actual shape of your API rather than the imagined one.
Decide who owns a contract failure before you turn the gate on. The first week of contract testing produces a flood of findings, and if nobody owns triage the team’s response will be to make the job non-blocking — after which it is decoration. Agree in advance that a schema-conformance failure is a producer bug by default, that an undocumented status code is a specification bug by default, and that either one blocks the merge. The rule matters less than having one, because the alternative is a per-failure argument that the gate always loses.
Run the exhaustive sweep on a schedule, the fast sweep on every change. Property-based testing is unbounded work: more generated examples find more bugs, and there is no natural stopping point. Cap it hard on pull requests — forty examples per operation is enough to catch drift, and it finishes in a couple of minutes — then run a nightly job with a much higher budget against staging, where a twenty-minute run costs nothing. The nightly job is where the genuinely obscure findings come from: the Unicode edge case, the integer boundary, the combination of optional parameters nobody thought to try together.
Keep the mock and the console pointed at the same document. It is easy to end up with a mock built from dist/api.bundled.yaml and a portal built from openapi/api.yaml, at which point they can disagree without anything failing. Bundle once, early, and have every downstream consumer — mock, portal, tests, SDK generation — read that one artifact. This is the same single-artifact discipline that makes preview deployments trustworthy in Docs CI/CD & Preview Deployments, applied to the contract instead of the site.
Fail the build on undocumented responses, not just wrong ones. The default instinct is to document whatever the API happens to return. Invert it: decide what the API should return, then make the implementation match. An undocumented 500 is a bug in the implementation far more often than a gap in the specification.
Verification & Testing
Prove each half independently before trusting the pair.
# 1. The mock serves what the spec promises, including named examples.
npx prism mock dist/api.bundled.yaml --port 4010 &
sleep 2
curl -s http://127.0.0.1:4010/invoices/inv_1 -H 'Prefer: example=paid' | jq -e '.status == "paid"' \
&& echo 'OK mock returns the named example'
# 2. The real API conforms. Expect failures on the first run — that is the point.
schemathesis run dist/api.bundled.yaml \
--base-url https://staging.api.acme.com \
--checks all --hypothesis-max-examples 20
A representative first-run failure looks like this, and it is the most valuable output in this guide:
FAILED GET /invoices/{id} [status_code_conformance]
Received: 500
Documented: 200, 404
Reproduce with: curl -X GET 'https://staging.api.acme.com/invoices/%00'
The reproduction command matters as much as the finding. Schemathesis prints the exact request that broke the API, so the failure goes straight into a bug report or a regression test without anyone having to reconstruct the input.
# 3. Staging traffic conforms continuously.
npx prism proxy dist/api.bundled.yaml https://staging.api.acme.com --errors
Each check catches a different class of defect, and none of them catches another’s. Knowing which is which stops the common mistake of adding a second tool that overlaps the first while the real gap stays open:
Troubleshooting
The mock returns "string" for every field. The operation has no examples, so Prism generated values from the schema. Add examples to the response content; this is a specification improvement that helps readers and renderers as much as it helps the mock.
Schemathesis reports hundreds of failures on the first run. Almost always undocumented error responses rather than genuine schema violations. Triage by check type — fix the documentation where the behaviour is correct, fix the implementation where it is not, and do not suppress the check.
Contract tests pass but consumers still break. The tests exercise shape, not behaviour. A response can be perfectly schema-conformant and semantically wrong. Keep behavioural tests for the logic, and use contract testing for the interface.
The mock and the real API disagree about required fields. The spec marks a field required that the implementation omits, or vice versa. This is exactly the drift the pairing exists to find; trust the specification and fix the implementation, or change the specification deliberately and treat it as a contract change under Breaking Change Detection & Spec Diffing.
Every generated request fails authentication. The test run is sending no credential, or one the staging environment rejects, so every operation returns 401 and the conformance checks report noise rather than findings. Pass a token explicitly, and prefer a dedicated test principal with broad read access and scoped write access over reusing a personal credential that will expire without warning. Confirm with a single manual curl before assuming the tool is misconfigured.
Generated writes pollute the test environment. Property-based testing will happily create thousands of invoices. Point it at an environment with a reset schedule, exclude genuinely destructive operations by tag, and treat any operation you must exclude as a candidate for mock-backed testing instead.
Property tests hang or time out. An unconstrained schema is generating pathological inputs, or the API is genuinely slow under generated load. Cap --hypothesis-max-examples in CI, tighten the schema constraints, and run the exhaustive sweep on a schedule rather than on every pull request.
FAQ
What is the difference between a mock server and contract testing?
A mock server plays the API for a consumer, returning responses derived from the specification so client work can start before the API exists. Contract testing points at a real implementation and checks that it obeys the specification. One unblocks consumers, the other prevents drift, and a serious pipeline uses both.
Will mocking hide bugs in the real API?
It will if the mock is the only thing your tests ever see. A mock proves your client matches the specification; it proves nothing about the server. Always pair mock-backed development with contract tests that run against a real deployed instance.
How do I generate useful mock data rather than empty strings?
Put examples in the specification. A mock derives responses from example values first and falls back to generating from the schema, so an operation with a realistic example returns realistic data and one without returns type-correct noise.
Can contract testing replace my integration tests?
No. Contract testing verifies the shape and status semantics of every documented operation, which is broad but shallow. It will not check that creating an invoice actually charges the right amount, so keep behavioural tests for the logic that matters.
Related
- OpenAPI & AsyncAPI Schema Authoring — the parent overview of contract authoring.
- Generating mock servers from OpenAPI with Prism — the mock setup in full.
- Contract testing OpenAPI with Schemathesis — property-based producer verification.
- Validating API responses against OpenAPI in CI — the always-on validation gate.
- Example Payload Management and Spec Linting & Governance — the examples and rules this depends on.