Skip to main content
Topic

API contract and negative testing

An API contract is what consumers rely on: the fields they read, the values each field may take, the status codes and error bodies, and who may call which operation. A contract test checks real responses against it. A negative test sends a request the contract says must be refused, and checks that it is refused for the reason the contract gives.

Derive the cases from the document instead of from the happy path. Every required field, bound, enum, pattern and security requirement in the OpenAPI description implies a request that should fail. Assert the status and the field the error points at, not the status alone, because a server can reject a request for the wrong reason and still return 400. Then diff the contract between versions, so a removed field or a narrowed enum fails CI before it fails a consumer.

Start from the spec or the collection you have

Both tools run in your browser and upload nothing.

Failure guides by cause

Each guide starts from the exact error string, then ranks the causes and gives the check that tells them apart. The full set is in the failure pattern library.

The response and the contract disagree

The service under test

Data the tests create

API Contracts in Practice, in order

The series this hub draws on, part by part.

  1. Part 1API Contract Testing with Playwright and JSON Schema

    Playwright's request fixture gives you a browser-free API suite, but a schema check only catches a renamed or vanished field if it sets additionalProperties: false and keeps the name in required. This walks through the open-schema trap, the Ajv settings that decide whether failures are readable, generating schemas from an OpenAPI document, why allOf silently reopens a closed schema, and the honest boundary between schema validation and consumer-driven contract testing with Pact.

  2. Part 2Turn a Postman Collection into Maintainable API Tests

    A Postman collection is an exploration artefact: its run order is an undeclared dependency, its variables are five rungs of shared mutable state, and its assertions are usually copied from one captured response. This walks the Collection v2.1 JSON to inventory what you actually have, shows the Postman script beside the migrated Playwright test, and argues for keeping the collection for documentation and mock servers rather than deleting it.

  3. Part 3Detect Breaking API Changes Before Consumers Do

    A line diff cannot tell whether an OpenAPI edit invalidates an old request, widens a response beyond a client's decoder, or removes behaviour a consumer depends on. This guide builds a deterministic compatibility gate around a released baseline, a semantic report, consumer ownership, and two focused executable contracts, then shows what evidence an intentional break needs before approval.

  4. Part 4Design Negative API Tests from the Contract

    A negative test that asserts only a 4xx status is satisfied by a crashed pod, a rewritten gateway response, and a rejection that already decremented stock. This guide derives one invalid request per contract rule, places each mutation on the boundary the rule actually defines, and gives every case a three-part oracle: the exact status code, the machine-readable location of the violation, and a probe proving no forbidden side effect survived the rejection.

  5. Part 5Test API Idempotency Under Retries and Timeouts

    A sequential double-submit test passes on services that double-charge in production, because the dangerous retry follows an outcome the client never observed. This guide constructs that ambiguity deliberately with an aborted in-flight request, separates the four replays that must produce four different answers, and shows why the durable ledger count is the assertion that fails when a cached response body would have passed.

  6. Part 6Test API Authorization Beyond Happy-Path Tokens

    A valid token answers who is calling and nothing else. This guide separates the object, property and function questions the way OWASP's 2023 edition does, reads the security declarations most OpenAPI descriptions get wrong, and builds a deny-by-default grid whose denials are proved by comparison against a fabricated ID and whose rejected writes are read back by an identity that can actually see the field.

  7. Part 7Verify Pagination, Filtering, and Sorting Contracts

    Asserting page lengths and spot-checking page one is true of an endpoint that loses records. This guide builds a reference set whose total order you control, walks it by following the Link header instead of assembling URLs, and asserts membership, uniqueness and sequence across the union — then inserts and deletes rows mid-traversal, which is exactly where offset pagination skips and duplicates.

  8. Part 8Review an AI-Generated OpenAPI Description Before You Publish It

    Structural validity and truthfulness about a service are unrelated properties: a generated description parses, resolves and lints while inventing a DELETE endpoint and quietly making the refund route public. This guide starts with the security override that removes authentication in two characters of YAML, reconciles operations against a route inventory taken from the booted router, and shows why the specification's SHOULD on examples means nothing is obliged to reject an invented field.

More on API contracts and negative tests

  • Build Deterministic Python API Test Data Factories

    A duplicate-key violation blamed on parallelism, an assertion broken by a Faker patch bump, and a 41-line payload diff with two relevant fields all come from the same cause: generating data without deciding what is fixed, what varies, and what the test asserts. This guide covers the reseed-to-zero behaviour of the faker fixture, why generated values must never appear as expected outputs, deriving reproducible unique identity from the test node id with uuid5, and the jsonschema format checker that does nothing unless you switch it on.

  • Check JSONPath Matches Before Writing the Assertion

    A path that selects the first order can pass while selecting the wrong record. Test a conditional JSONPath against a paid and unpaid order before keeping it in a test.

  • Find the Actual Change Between Two API Responses

    Two API responses look almost identical, but amount changed from a number to a string. Use JSON Diff to find that path, read array changes correctly, and decide which differences need a contract test.

  • How to Test an MCP Server: Protocol to Safety

    A drafts server that passes its happy-path tests can still write a draft after the client has timed out, because the official TypeScript client opens with the 2025 handshake by default and the stateless legacy path never delivers the cancellation. This guide tests an MCP server layer by layer against the 2026-07-28 specification with SDK 2.4.0: era pinning, raw envelope probes with real -32602, -32020 and -32022 responses, per-principal tools/list, refusals checked against storage, cancellation in both eras, and an audit log that records refused calls.

  • Inspect JWT Claims Without Trusting the Decode

    A decoded JWT can explain a 401, but it cannot prove that an API should accept the token. Check its structure, claims, and verification boundary.

  • Mock or Integrate? Playwright Network Testing Boundaries

    Mocking is a decision about which contract a test asserts on, not a shortcut for speed. This guide gives you the criterion for mocking versus integrating, the exact differences between fulfill, continue, fallback, abort and fetch, and a concrete way to catch mocks that have drifted away from the real API.

  • Review Required Fields in a Generated JSON Schema

    A schema inferred from one successful response may reject valid variants. Use the generator for observed types, then review required fields and closed-object rules against the API contract.

  • Review a cURL Conversion Before Replaying It

    The cURL converter gives you a request sketch, not a finished test. A simple POST exposes an invalid Python boolean in generated output.

  • Test TypeScript Error Handling Without Losing Causes

    Rethrowing new Error(e.message) turns a refused connection into two words and a stack that starts in your own catch block. This guide covers what the cause option actually installs, the subclass field that silently resets cause to undefined, an invoice client that classifies validation, unauthorized, timeout and unknown failures with fixed user copy and retry flags, a single redacting log boundary, and Vitest tests that assert each classification, including why toThrow with an error instance cannot see a lost cause.

  • Type Safety Stops at Runtime: Test the TypeScript Boundary

    A type assertion compiles away, and JSON.parse and Response.json() are typed any, so a payload missing a required role or carrying an unlisted status reaches your domain code unchecked. This guide covers why hand-written type predicates repeat the problem, parsing unknown through a Zod 4 schema into a separate domain type, when to strip or reject undeclared keys, why a missing enum field and an unknown enum value report the same issue code, and a Vitest suite that asserts path, code and a sanitised received value without leaking personal data into failures or test names.