Skip to content

Spec sync: drift & coverage

This is the feature TruSpec exists for. Your OpenAPI document is the source of truth; your collection is measured against it. Three checks — drift, coverage, and contract — keep them honest, run fully offline, and fail the build the moment your collection (or your API) rots away from your spec.

A fourth command, gen, closes the loop by generating a request stub for every operation so you start at full coverage.


The model

A request links to a spec operation with a spec block:

yaml
spec:
  operation: "GET /pets/{id}"   # "${METHOD} ${path}"
  operationId: getPetById       # preferred when present

TruSpec parses your OpenAPI 3 document into a flat list of operations (keyed METHOD path, e.g. GET /pets/{id}), then matches each request to an operation:

  • If both have an operationId, they match on it.
  • Otherwise the request's operation string is matched against the operation's METHOD path key.

Everything downstream — drift, coverage, the live probe — is computed from that matching.


Drift

truspec drift diffs your collection against the spec and exits non-zero when they've diverged. It reports four categories:

CategoryMeaningWhy it matters
Untracked (added)In the spec, but no request references it.A new endpoint shipped with no test.
Stale (removed)Referenced by a request, but gone from the spec.A request points at an endpoint that no longer exists.
Changed (changed)Matched, but the request no longer satisfies the spec.The contract tightened — e.g. a parameter became required — and the request didn't keep up.
Missing from live API (liveMissing)With --live, a spec operation a running API doesn't serve.The deployed API and the spec disagree.

"Changed" currently fires when:

  • the spec marks a query parameter as required and the request doesn't include it, or
  • the spec marks the request body as required and the request has no body.
bash
truspec drift --spec openapi.yaml ./api
Spec operations: 4   Collection operations: 3

Untracked in collection (1):
  + GET /users/{id}

Drift detected: 1 untracked, 0 stale, 0 changed.

The drift check passes (exit 0) only when all four categories are empty.

Probing a live API (--live)

Add --live <baseUrl> to also check a running API for operations it doesn't serve. This catches the case where the spec and the deployment have diverged — the spec promises an endpoint that returns 404/405, or the host is unreachable.

bash
truspec drift --spec openapi.yaml ./api --live https://api.staging.example.com

For safety against a production target, the live probe only sends GET and HEAD requests; mutating operations are skipped (and reported as skipped). Path parameters are filled with 1 to form a concrete URL. An operation is flagged "missing from live API" when the probe returns 404, 405, or the host is unreachable. Use --timeout <ms> to bound each probe.

JSON shape

--json emits a DriftReport:

json
{
  "specOperations": 4,
  "collectionOperations": 3,
  "added": ["GET /users/{id}"],
  "removed": [],
  "changed": [],
  "liveMissing": [],
  "ok": false
}

Coverage

truspec coverage reports what share of spec operations are exercised by a request that also has assertions — a request with no assertions doesn't count, because it asserts nothing about the contract.

bash
truspec coverage --spec openapi.yaml ./api
Coverage: 75% (3/4 operations tested)

Uncovered (1):
  ✗ GET /users/{id}

Gate on a minimum with --min:

bash
truspec coverage --spec openapi.yaml ./api --min 80   # exit 1 if below 80%

percent is round(covered / total * 100); an empty spec reports 100%. The command exits 0 when percent >= --min (default 0, i.e. report-only).

JSON shape

json
{
  "total": 4,
  "covered": ["GET /pets", "GET /pets/{id}", "POST /pets"],
  "uncovered": ["GET /users/{id}"],
  "percent": 75,
  "ok": true
}

Response validation (contract)

drift and coverage are static — they reason about whether a request references an operation and supplies its required inputs. They never look at what the API actually returns. truspec contract closes that gap: it runs the collection and validates each response body against the spec's response schema for the matched operation.

bash
truspec contract --spec openapi.yaml ./api --env local
Contract: 2/3 tested operations conform to the spec
  ✓ GET /posts
  ✓ GET /posts/{id}

Violations (1):
  ✗ POST /posts  →  schema: 1 violation(s) — /author/id: missing required property 'id'

Untested — no request exercises these (1, see `coverage`):
  – GET /users/{id}

Contract violations: 1.

Because it sends requests, contract takes the same knobs as run--env for the base URL and secrets, --timeout, --json. Point it at a mock or a real API. It exits non-zero only on a schema violation; untested and status-undocumented operations are reported but don't fail the gate (that's coverage/drift's job).

This is behavioral drift — the kind the structural drift check can't catch. A handler that returns a string id where the spec promises an integer, drops a required field, or adds an undeclared status passes every existing status/jsonpath assertion but fails contract.

Validated without a separate command

You don't need the contract command to get response validation. Two lighter paths:

  • truspec run --spec openapi.yaml ./api — every spec-linked request is validated against its response schema inline, as an extra assertion in the normal run output.
  • { type: schema } assertion — opt a single request in (or pin a status / contentType). See File format → schema.

The validator covers the OpenAPI 3 schema subset (type, properties, required, items, enum, nullable, allOf/oneOf/anyOf, $ref, additionalProperties: false) and reports the JSON path of every violation. format is treated as an annotation (not enforced).

JSON shape

--json emits a ContractReport:

json
{
  "specOperations": 4,
  "conformed": ["GET /posts", "GET /posts/{id}"],
  "violations": [
    {
      "op": "POST /posts",
      "status": 201,
      "message": "schema: 1 violation(s) — /author/id: missing required property 'id'",
      "filePath": "api/create-post.tspec.yaml"
    }
  ],
  "skipped": [],
  "untested": ["GET /users/{id}"],
  "ok": false
}

Scaffolding from a spec

truspec gen writes a request stub for every operation in your spec — each with a status: 200 assertion and a spec link already filled in. It's the fastest way to take a brand-new spec to a fully drift-tracked collection.

bash
truspec gen --spec openapi.yaml --out ./api

Each generated file looks like:

yaml
tspec: "0.1"
name: getPetById
method: GET
url: "{{baseUrl}}/pets/{{id}}"
assertions:
  - { type: status, equals: 200 }
spec:
  operation: "GET /pets/{id}"
  operationId: getPetById
  • Path parameters become template variables ({id}{{id}}).
  • The base-URL variable defaults to baseUrl; override with --base-url-var.
  • File names are slugified from the operationId (or the METHOD path key).
  • Operations with an unsupported HTTP method are skipped and reported.

After gen, flesh out the stubs (real assertions, bodies, auth) and your drift count drops to zero. Agents can do this too — see the truspec_scaffold_from_spec MCP tool.


A complete workflow

Put the three together and your API contract becomes a build gate:

bash
# 1. Bootstrap a collection from the spec.
truspec gen --spec openapi.yaml --out ./api

# 2. Fill in assertions / bodies, then run against a mock or a real API.
truspec mock --spec openapi.yaml &
truspec run ./api --env local

# 3. Gate CI on the contract.
truspec drift    --spec openapi.yaml ./api              # no new/removed/changed endpoints
truspec coverage --spec openapi.yaml ./api --min 80     # enough of the surface is tested
truspec contract --spec openapi.yaml ./api --env local  # responses match the spec's schemas

When someone adds an endpoint to the spec, drift fails until a request covers it. When someone makes a parameter required, drift flags the now-incomplete request. When the deployed API and the spec disagree, drift --live catches it. When a handler returns a shape the spec doesn't allow, contract catches it. That's the whole point: the collection — and the API behind it — can't silently fall out of sync.

See the CI/CD guide to wire these into GitHub Actions and other pipelines.


See also

Released under the MIT License.