Contract-Driven Mocking with Microcks 1.10
Hand-written mocks start reasonable and then drift. A developer copies a payload from a design doc, the schema evolves, the mock does not, and six months later the consumer’s integration tests pass while the real provider rejects the same requests. The failure surfaces at the worst possible moment — during an integration week or a production incident — because no automated check ever compared the stub to the authoritative contract.
This guide is part of the Mock Server Strategies cluster. It covers a concrete fix: use Microcks 1.10 to import your OpenAPI and AsyncAPI documents as both live mock servers and CI conformance test suites, so the mock is permanently tethered to the artifact it was derived from.
Symptom
The most common symptoms of mock drift appear in integration tests and in CI logs:
- Consumer tests pass against the local stub but fail against the deployed provider with
400 Bad Requestor422 Unprocessable Entity, because the stub accepted a field the schema marks asreadOnlyor rejected byadditionalProperties: false. - Response payloads returned by the mock include fields that were removed from the schema months ago. Consumers build UI components against those phantom fields, which are absent in production responses.
- AsyncAPI event consumers receive hand-typed JSON messages from a test helper that no longer matches the current channel schema, masking deserialization bugs until real messages arrive.
- A stub updated by one team member silently diverges from the contract reviewed by another, because there is no automatic check that the stub’s shape is a valid instance of the response schema.
The common thread: the mock is an island. It was created from the contract but has no ongoing relationship to it.
Root Cause
Hand-written mocks drift because the artifact they were derived from is not the artifact that drives them at runtime. The spec lives in one repository, the stub lives in another, and the only thing keeping them in sync is developer discipline — which does not survive team growth, time pressure, or context switching.
Microcks eliminates the gap by making the OpenAPI or AsyncAPI document the live mock configuration. When you import a document, Microcks parses every examples block and constructs a dispatch table: each named example becomes an addressable mock endpoint that returns that exact payload. When the document changes, you re-import; the mock updates atomically. There is no separate stub file to maintain.
The conformance test runner goes further: for each imported example it sends the example request to the mock and validates the response body against the JSON Schema in the document. A schema change that makes an existing example invalid is caught at import-validate time, not at integration time.
Step-by-Step Fix
1. Add Named Examples to Every OpenAPI Response
Microcks derives its mocks from the examples objects in your OpenAPI document. An operation with no examples gets no mock. Place at least one named example under every significant response — use realistic values that pass validation, because the conformance runner will check them.
# openapi.yaml — OpenAPI 3.1.0
openapi: 3.1.0
info:
title: Payments API
version: 2.0.0
paths:
/payments/{id}:
get:
operationId: getPayment
parameters:
- name: id
in: path
required: true
schema:
type: string
pattern: '^pay_[a-z0-9]{16}$'
responses:
'200':
description: Payment record
content:
application/json:
schema:
$ref: '#/components/schemas/Payment'
examples:
# Each named example becomes an independently addressable mock.
# Microcks uses the example name in its dispatch URL.
settled:
value:
id: pay_a1b2c3d4e5f6g7h8
status: SETTLED
amountMinor: 8500
currency: EUR
pending:
value:
id: pay_z9y8x7w6v5u4t3s2
status: PENDING
amountMinor: 3200
currency: USD
'404':
description: Payment not found
content:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetail'
examples:
notFound:
value:
type: /problems/payment-not-found
title: Payment Not Found
status: 404
detail: No payment with id pay_a1b2c3d4e5f6g7h8 exists.
/payments:
post:
operationId: createPayment
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreatePaymentRequest'
examples:
euroPayment:
value:
amountMinor: 8500
currency: EUR
reference: INV-2026-0042
responses:
'201':
description: Payment created
content:
application/json:
schema:
$ref: '#/components/schemas/Payment'
examples:
euroPayment:
# Request and response examples share a name so Microcks
# pairs them automatically into one mock interaction.
value:
id: pay_a1b2c3d4e5f6g7h8
status: PENDING
amountMinor: 8500
currency: EUR
components:
schemas:
Payment:
type: object
additionalProperties: false
required: [id, status, amountMinor, currency]
properties:
id:
type: string
status:
type: string
enum: [PENDING, SETTLED, FAILED]
amountMinor:
type: integer
description: Amount in the currency's minor unit (e.g. cents)
currency:
type: string
pattern: '^[A-Z]{3}$'
CreatePaymentRequest:
type: object
additionalProperties: false
required: [amountMinor, currency, reference]
properties:
amountMinor:
type: integer
currency:
type: string
pattern: '^[A-Z]{3}$'
reference:
type: string
ProblemDetail:
type: object
required: [type, title, status]
properties:
type:
type: string
title:
type: string
status:
type: integer
detail:
type: string
Why this works: Microcks reads the examples block directly — it does not fabricate data from the schema. Named examples give deterministic, assertable payloads and prevent the mock from returning a shape the document never described.
2. Start Microcks via Docker
The microcks-uber image bundles the mock server, the import engine, and the test runner into a single container. No external database is required for local and CI use.
# Pull the exact image version so CI behaviour is reproducible.
docker pull quay.io/microcks/microcks-uber:1.10.0
# Start in detached mode; expose port 8585 on the host.
docker run -d \
--name microcks \
-p 8585:8080 \
quay.io/microcks/microcks-uber:1.10.0
# Wait for the health endpoint — do not import before the server is ready.
until curl -sf http://localhost:8585/api/health; do
sleep 2
done
echo "Microcks is ready"
Why this works: The uber image avoids the multi-container compose setup needed for production deployments, which keeps CI fast and dependency-free. The health poll prevents race-condition failures where an import arrives before the HTTP listener is bound.
3. Import the OpenAPI Artifact
POST the document to Microcks’s import endpoint. Microcks parses the title and version from the info block, registers the API, and activates a mock endpoint for each named example.
# Import the OpenAPI document.
# The mainArtifact=true flag tells Microcks this is the authoritative contract
# for this API+version combination, not a secondary reference.
curl -s -X POST \
'http://localhost:8585/api/artifact/upload?mainArtifact=true' \
-F 'file=@openapi.yaml' \
| jq .
# Expected response:
# "Payments API [2.0.0] imported"
After import, Microcks exposes mock endpoints at the pattern /rest/<API+name>/<version>/<path>. Spaces in the API name are encoded as +:
# Verify the API was registered and its operations are visible.
curl -s 'http://localhost:8585/api/services?q=Payments' | jq '.[].operations[].name'
# => "GET /payments/{id}"
# => "POST /payments"
Why this works: Microcks parses the entire contract on import, not on first request. Any structural problem in the document — a missing $ref target, a malformed example — surfaces as an import error you can fix before the mock is ever called.
4. Smoke-Test the Mock Endpoints
Confirm Microcks returns the correct named example for each operation. The default dispatch rule for a GET path selects the example whose name matches the last path segment, falling back to the first example in document order.
# Retrieve the "settled" example — Microcks matches the path suffix.
curl -s 'http://localhost:8585/rest/Payments+API/2.0.0/payments/settled' | jq .
# => { "id": "pay_a1b2c3d4e5f6g7h8", "status": "SETTLED", "amountMinor": 8500, "currency": "EUR" }
# Retrieve the "pending" example.
curl -s 'http://localhost:8585/rest/Payments+API/2.0.0/payments/pending' | jq .
# => { "id": "pay_z9y8x7w6v5u4t3s2", "status": "PENDING", "amountMinor": 3200, "currency": "USD" }
# Trigger the 404 example via a path suffix that matches the example name.
curl -s 'http://localhost:8585/rest/Payments+API/2.0.0/payments/notFound' | jq .status
# => 404
# POST to create: Microcks pairs request and response examples by name.
curl -s -X POST \
'http://localhost:8585/rest/Payments+API/2.0.0/payments' \
-H 'Content-Type: application/json' \
-d '{"amountMinor": 8500, "currency": "EUR", "reference": "INV-2026-0042"}' \
| jq .status
# => "PENDING"
Why this works: The smoke test proves the import succeeded and the dispatch rules resolved correctly before you wire the mock into any consumer tests. A 400 or a mismatched payload here points to a naming mismatch between request and response examples, fixable in the source document.
5. Run the Built-In Conformance Test Suite with microcks-cli
The conformance test runner replays every imported example against the live mock and validates each response against the JSON Schema. This is the CI gate that replaces the manual audit.
Install microcks-cli 0.5.x:
# microcks-cli is a single Go binary; grab the Linux amd64 build.
curl -sSL \
https://github.com/microcks/microcks-cli/releases/download/0.5.0/microcks-cli-linux-amd64 \
-o /usr/local/bin/microcks-cli
chmod +x /usr/local/bin/microcks-cli
Run the test:
microcks-cli test \
'Payments API:2.0.0' \
http://localhost:8585/rest/Payments+API/2.0.0 \
OPEN_API_SCHEMA \
--microcksURL=http://localhost:8585/api \
--waitFor=10s \
--keycloakClientId=foo \
--keycloakClientSecret=bar
Successful output:
Test "Payments API - 2.0.0" STARTED for target URL: http://localhost:8585/rest/Payments+API/2.0.0
...
Test "Payments API - 2.0.0" is SUCCESSFUL !
- GET /payments/{id}: 2 example(s), 2 SUCCESS
- POST /payments: 1 example(s), 1 SUCCESS
A failure line looks like:
- GET /payments/{id}: 2 example(s), 1 SUCCESS, 1 FAILURE
[settled] Response body does not validate against schema:
additionalProperties: legacyField is not defined in schema
Why this works: The test runner uses the OPEN_API_SCHEMA strategy, which validates response bodies against the components/schemas JSON Schema. A field that was removed from the schema but left in an example becomes a CI failure — drift is caught automatically on every import.
Before / After
Before Microcks: a hand-maintained stub file in the consumer repository.
// stubs/payment.json — checked in, never reconciled with the spec
{
"id": "pay_abc123",
"status": "SETTLED",
"amount": 8500,
"legacyField": "xyz",
"currency": "EUR"
}
The schema removed legacyField and renamed amount to amountMinor four months ago. The stub still has both the old field and the old name. Consumer tests pass; integration tests against the real provider fail with 422 Unprocessable Entity.
After Microcks: the OpenAPI document is the stub.
# openapi.yaml (same file the provider team owns)
# examples:
# settled:
# value:
# id: pay_a1b2c3d4e5f6g7h8
# status: SETTLED
# amountMinor: 8500 # renamed field; the old name is gone
# currency: EUR # legacyField absent; schema has additionalProperties: false
# CI import step — any example that violates the schema fails the build.
curl -s -X POST \
'http://localhost:8585/api/artifact/upload?mainArtifact=true' \
-F 'file=@openapi.yaml'
microcks-cli test 'Payments API:2.0.0' \
http://localhost:8585/rest/Payments+API/2.0.0 \
OPEN_API_SCHEMA \
--microcksURL=http://localhost:8585/api \
--waitFor=10s \
--keycloakClientId=foo \
--keycloakClientSecret=bar
# => SUCCESSFUL — if an example slips in with legacyField, the run fails here.
The consumer now points its integration tests at http://localhost:8585/rest/Payments+API/2.0.0 and the mock is regenerated from the same document the provider is validated against.
Verification
In CI, the Microcks test run exits non-zero on any conformance failure. Gate the pipeline on that exit code:
# .github/workflows/contract.yml
jobs:
contract-test:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Start Microcks
run: |
docker run -d --name microcks -p 8585:8080 \
quay.io/microcks/microcks-uber:1.10.0
until curl -sf http://localhost:8585/api/health; do sleep 2; done
- name: Import contract
run: |
curl -sf -X POST \
'http://localhost:8585/api/artifact/upload?mainArtifact=true' \
-F 'file=@openapi.yaml'
- name: Run conformance tests
run: |
microcks-cli test 'Payments API:2.0.0' \
http://localhost:8585/rest/Payments+API/2.0.0 \
OPEN_API_SCHEMA \
--microcksURL=http://localhost:8585/api \
--waitFor=30s \
--keycloakClientId=foo \
--keycloakClientSecret=bar
- name: Run consumer integration tests against mock
run: npm test # tests target http://localhost:8585/rest/Payments+API/2.0.0
A green run guarantees that every named example in the contract is a valid instance of the schema, and that consumer tests exercising those examples would pass against any provider that also satisfies the contract.
Edge Cases & Caveats
-
Dispatch rule collisions. When two examples share a name across different response codes (for example, a
200settledand a404settled), Microcks uses the HTTP method and the response status declared in the spec to distinguish them. If the path suffix alone is ambiguous, add explicit dispatch rules viamicrocks-metadata.yamloverlays — aQUERY_PARAMorBODY_CONTENTdispatcher routes by query string or request body field value instead. -
AsyncAPI event-driven mocking. Microcks 1.10 imports AsyncAPI 3.0 documents and publishes example messages to Kafka, MQTT, WebSocket, and Amazon SQS bindings. The conformance test runner covers those channels as well. Pair this with the AsyncAPI for Event-Driven Systems guide when your API spans both HTTP and event channels: one Microcks instance mocks both surfaces from separate artifacts.
-
Auth in the uber image. The microcks-uber container ships with Keycloak disabled by default; the
--keycloakClientIdand--keycloakClientSecretflags must still be passed tomicrocks-clibut their values are ignored. In a secured Microcks deployment (production, shared environments), obtain a real service-account token and pass it. Do not disable auth in a shared environment — the import endpoint accepts arbitrary artifacts.
Frequently Asked Questions
Does Microcks support AsyncAPI as well as OpenAPI?
Yes. Microcks 1.10 imports AsyncAPI 3.0 documents and publishes example messages to Kafka, MQTT, WebSocket, and Amazon SQS bindings. Declare your channel bindings and payload examples in the AsyncAPI document and Microcks will act as a live broker stub.
What is the difference between Microcks mocking and Prism mocking?
Prism synthesizes responses from the schema or named examples in an OpenAPI document and validates traffic in proxy mode. Microcks replays the exact negotiated interactions captured in the artifact, which means the mock can never return a shape the spec did not document. For conformance testing Microcks also ships a built-in test runner; Prism relies on the proxy mode and external test frameworks. For a direct side-by-side comparison, see WireMock vs Prism for integration testing.
Can Microcks run in CI without a persistent server?
Yes — the microcks-uber Docker image bundles the mock server and test runner into a single container. Spin it up, import the artifact, run the conformance tests, capture the exit code, and tear it down. The microcks-cli simplifies this to a single command.
How does Microcks route an incoming request to the right example?
Microcks evaluates dispatch rules in priority order: URI parts first, then query parameters, then request body content. You define these rules per-operation in the Microcks UI or via a microcks-metadata.yaml overlay. An inbound request that matches no rule gets a 400 with a dispatch-failure message.
What happens if my OpenAPI examples contain a field the schema does not allow?
Microcks imports the examples verbatim; it does not re-validate them against the schema at import time. The conformance test runner does validate example payloads against the schema during a test run and will report a FAILURE for any example that violates additionalProperties: false or a type constraint. Fix the example in the source document, re-import, and re-run.