How-to guidesMock and contract-test an API

Mock and contract-test an API

A contract is only useful while both sides keep it. This guide serves an API from its OpenAPI document, openapi.yaml, so that client teams can start before the server exists, and then checks the real server’s traffic against the same document, locally and in CI. apigenie-server does the same for AsyncAPI documents (WebSocket, HTTP and MQTT), .proto files (gRPC) and GraphQL schemas; see the server reference.

You need the CLI installed; the Quickstart shows how.

Serve a mock

apigenie-server openapi.yaml
Listening on http://127.0.0.1:4010/v1 (spec: openapi.yaml, 12 operations)
Admin API: http://127.0.0.1:4010/__apigenie
Dashboard: http://127.0.0.1:4010/__apigenie/ui

The mock serves the paths of the document under the path of its first server (/v1 here), checks every request against the document, and answers with the document’s examples. Operations without examples get a body generated from their schemas; --dynamic generates every answer. --watch reloads the mock when the document changes, so it follows the contract as you write it.

Open the dashboard to see each request, what answered it and what was wrong with it.

Ask for the answers you need

A client has to handle more than the happy path. The Prefer header, or a query parameter, picks the answer of one request:

curl http://127.0.0.1:4010/v1/orders -H 'Prefer: code=404'
curl http://127.0.0.1:4010/v1/orders -H 'Prefer: example=empty'
curl http://127.0.0.1:4010/v1/orders -H 'Prefer: delay=1500'

For answers that should hold for a while, such as an outage of one service, define scenarios in the document or in the configuration, and switch them on with --scenario or in the dashboard:

paths:
  /orders:
    get:
      x-apigenie-mock:
        scenarios:
          empty: {body: []}
          broken: {status: 500, drop: {probability: 0.5, mode: reset}}

--latency, --jitter and --drop-rate make every answer slow or unreliable, and --seed repeats the same run.

Check the real server

Once the server runs, put apigenie-server in front of it with --proxy-to. Every request goes to the real server, and the request and the server’s response are both checked against the document:

apigenie-server openapi.yaml --proxy-to http://localhost:8080

Point your tests, your front end or a load test at the proxy instead of the server, and their traffic becomes a contract test. Each response carries the problems found in its exchange in the X-Apigenie-Mock-Violations headers, and the dashboard’s Contract screen shows the coverage and the violations by check.

To send the requests yourself rather than through your tests, apigenie test runs .http files, or one generated request per operation, against the server with the same checks:

apigenie test --spec openapi.yaml --generate --base-url http://localhost:8080

Fail CI when the contract breaks

In CI, start the proxy, run the test suite through it, and stop it. On stopping it writes the report and exits with status 5 when the contract failed:

apigenie-server openapi.yaml --proxy-to http://localhost:8080 --port 4010 \
  --report contract.json --report-junit contract.xml --fail-on errors --min-coverage 80 &
API_URL=http://localhost:4010 npm test
curl -X POST http://localhost:4010/__apigenie/shutdown
wait $!

--fail-on errors fails on any error-level violation, and --min-coverage 80 when the tests leave more than a fifth of the document untested. Most CI systems show the JUnit report, one test case per operation, next to the other test results.

Record once, replay in tests

Integration tests that call other services are slow and flaky. --record DIR writes every exchange of a session to a directory, and --replay DIR answers from it later, so the tests run without the real services:

apigenie-server openapi.yaml --proxy-to https://api.example.com --record sessions/checkout
apigenie-server openapi.yaml --replay sessions/checkout

See session recording for what is recorded and how secrets are kept out of the files.

Next steps