Skip to main content

Contract Testing

A contract records what a consumer actually relies on from a provider API: the requests it really sent and the responses it really received, fully materialized. The provider replays those interactions against its own implementation, so a breaking change is caught in the provider's pipeline instead of surfacing as a red build in every consumer's.

It complements your test cases rather than replacing them:

A test caseA contract
Checked againstthe imported API specificationwhat a specific consumer observed
Runs inthe consumer's pipelinethe provider's pipeline
Fails whenthe API stopped matching the spec or your expectationsthe provider stopped satisfying a real consumer

A Contract Is a File, Not a Service​

A contract is a plain JSON document that lives next to the project that produced it:

Booking/
├── Booking.aprj
├── contracts/
│ └── Booking-Flow.contract ← committed to the repository
└── ...

So contract testing needs no server, no login and no network:

  • the expectations are the file;
  • the structure comes from the provider's own installed extension — the object model generated from its specification, which is what the typed check compares against;
  • exchange is git: the consumer commits the file, the provider (or a shared repository) carries it.
note

Publishing contracts to the Extension Repository was considered and rejected: it would have made verification depend on a running service and moved recorded request/response bodies off the developer's machine, for a problem a committed file already solves (decision 0039).

Exporting From Studio​

Right-click a test case in the Test Automation tree and choose Export contract…. The dialog lists what that case recorded during its last run, asks who the consumer is and how strict the provider's check should be, and writes the file into the project's own contracts folder.

Contract export dialog showing two recorded interactions, the consumer name, the verification level and the file being written

A case that has not been executed yet has nothing to record, so the dialog says so instead of offering an empty contract. Anything worth confirming before you commit the file is raised as a warning in the same dialog — an interaction without an operation id, a missing provider service id, or a recorded error status that the provider will now be held to.

Export dialog with warnings: a recorded 404 the provider will now be held to, and a contract that already exists

Verifying From Studio​

Once the project's contracts folder holds at least one file, a Contracts node appears under the project in the Test Automation tree, listing one row per contract file. Right-click the node for Verify contracts…, Refresh (re-read the folder) and Open folder; right-click a single contract for Verify this contract. The project node's own context menu carries the same Verify contracts… entry.

The verification dialog lists the contracts the project carries — consumer, interaction count, strictness — lets you pick the environment and optionally override the strictness or the target URL, and replays every interaction against the installed extensions.

Contract verification dialog listing two contracts with their consumer and strictness, above the environment and strictness controls

A failing interaction is reported with the assertion that stopped matching, so the answer to "what did we break?" is in the dialog rather than in a diff you have to reconstruct. A contract naming an operation the provider no longer defines is marked invalidated and is not replayed.

:::note The node is cheap by design The Contracts node appears only while the folder holds files, a row shows a file name and nothing else — reading a contract to draw a row would cost every project that has one — and the folder is re-listed when you touch the node or export a contract, not watched on disk. :::

Quickest Scenario​

# 1. Consumer: run the tests and record what they exercised (into Booking/contracts/)
aeth run ./Booking.aprj --env staging --export-contracts

# 2. Commit the file to wherever the provider can see it (its repo, or a shared one), e.g.
# into Airline/contracts/Booking-Flow.contract

# 3. Provider: verify your implementation against every contract you carry
aeth verify-contract --project ./Airline.aprj --env staging

verify-contract exits 0 only when every contract verifies, and 1 when an interaction fails or a contract references an operation the provider no longer defines.

Export runs together with the test run on purpose: responses are runtime state and are not stored inside the project file, so a contract can only be recorded while the requests are actually made.

How Strict Should the Check Be​

The strictness is stored in the contract and can be overridden per run with --strict:

LevelCLI valueWhat must match
Status onlystatusThe HTTP status code of each interaction
Typed conformance (default)typeStatus, plus the response body satisfies the structure defined for that status code in the imported specification
Exact membersexactThe above, plus the response body equals the recorded body (JSON compared member by member)

type is the recommended level: it fails when a field a consumer depends on disappears or changes type, while tolerating fields you have added. Use exact where the response shape must not drift.

When a Change Invalidates a Contract​

Before anything is sent, each recorded operation is resolved against the provider project's installed extensions. A removed or renamed operation invalidates its contract and is reported instead of being quietly skipped:

Verifying 'Booking Flow' (3 interaction(s), strictness TypeConformant) from Airline/contracts/Booking-Flow.contract.
invalidated by the provider's current extension:
operation not found: Hummingbird.Airline.GET /api/v1/pet/{id} — regenerate the contract from the consumer's current test case.

Nothing is replayed in that case: a contract naming operations that no longer exist cannot be verified honestly, so the consumer regenerates it.

A changed specification is not the same as a failed contract​

Each contract also records the hash of the specification the provider's extension was generated from. When the provider now installs an extension built from a different specification, the contract is annotated:

specification changed since this contract was recorded (a1b2c3d4e5f6 → d4e5f6a1b2c3) —
a failure may be an intended change; review it, then regenerate the contract

That annotation never fails a run by itself. It changes what a failure means: with no drift, a failing interaction is a regression against what the consumer depends on; with drift, it is plausibly a change the provider meant to make, and the decision is to regenerate the contract (accepting the change) or to revert it. Contracts recorded before this field existed, and extensions published as legacy binary packages without specification provenance, report no drift either way — the absence of a hash is not treated as a match.

Checking Conformance From Inside a Test Case​

The same rule is available as a step assertion. Add the Schema Conformance Assertion to a Send Request step to fail that step when the response no longer matches what the specification defines for the returned status code — field-level violations show up in the step diagnostics, for example Pet.Name: required field is null. A status code the specification does not describe is reported but not treated as a failure. See Assertions.

Contract File Reference​

FieldMeaning
nameDisplay name — the test case that recorded it
consumerConsumer identity recorded in the document
providerServiceIdProvider service the interactions belong to
specHashHash of the consumer's installed provider extension at record time (drift signal)
strictnessStatusOnly, TypeConformant or ExactMembers
interactions[]name, operationId, method, uri, contentType, headers[], requestBody, expectedStatusCode, expectedBody
recordedAtUtcWhen the interactions were captured

Test cases that made no requests produce no file. File names come from the test case name, with spaces replaced by hyphens.

Command Reference​

aeth run <project> --export-contracts [<dir>]​

Writes one .contract file per test case that recorded at least one interaction. With no directory, the project's own contracts folder is used — which is the default you want, because it puts the contract under version control next to the tests that produced it.

aeth verify-contract [<contract-file>] [options]​

OptionDescription
--project <aprj>, -pRequired. Provider automation project whose installed extensions define the service
<contract-file>Verify this one file instead of the project's contracts folder
--contract <name>Within the folder, verify only this contract (file name or contract name)
--key <key>, -kEncryption key of a password-protected provider project
--env <name>, -eTest environment of the provider project to run against
--url <url>Replace the URL recorded in every interaction, to point at another host or port
--strict status|type|exactOverride the strictness stored in the contracts

What a Contract Carries, and Where It Goes​

A contract holds recorded request and response bodies, which in practice means tokens, payload samples and internal host names. Keep that in mind when you choose the repository it is committed to — the same place you would commit a fixture, not a public one — and regenerate rather than hand edit when the provider changes.

See Also​