Here is the uncomfortable one. Most API test suites pass because they are testing a mock that the same developer wrote, from the same wrong mental model, on the same afternoon. The suite is green, coverage is 85 percent, and the integration still breaks in staging because the server returns created_at as a Unix timestamp and the mock returned an ISO string. Nothing in the test run could have caught that.
A mock encodes an assumption. If the assumption is wrong, the mock is wrong in exactly the same way the client is, and the two agree perfectly forever. The fix is to stop hand-writing the mock and derive it from something the server also derives from. In practice that means one of three things: validate responses against the OpenAPI spec inside the assert step so a drift between spec and implementation fails a test; generate the mock from the spec, so changing the spec breaks the client tests immediately; or use consumer-driven contract testing with Pact, where the consumer publishes the interactions it relies on and the provider's CI replays them and fails if it no longer satisfies them.
Schemathesis is the cheapest version of this to adopt. Point it at a spec and it generates requests, including nasty edge cases, and asserts that every response conforms to the declared schema and status codes. Ten minutes of setup finds undocumented 500s in nearly any spec that has never been fuzzed.
Snapshot-asserting an entire response body is the reason additive, backwards-compatible changes break test suites. Adding an optional field should never fail a consumer. Assert three layers instead: the status code, the response schema, and the two or three field values the test is actually about.
GET https://api.example.com/v1/orders/42
Authorization: Bearer {{token}}
HTTP 200
[Asserts]
header "Content-Type" contains "application/json"
header "Cache-Control" exists
jsonpath "$.id" == 42
jsonpath "$.status" in "pending" "paid" "shipped"
duration < 800
That is Hurl, which stores tests as plain text files that diff cleanly in review. Bruno takes the same offline, file-in-the-repo approach for teams that want a GUI. Postman collections run headlessly through Newman if you already live there, but a collection stored in someone's cloud workspace is not reviewable, and unreviewable tests rot.
The second under-discussed thing is that most flaky API suites are not flaky. They are order-dependent. Test A creates user test@example.com, test B assumes it exists, test C deletes it, and the failure only appears when the runner shards differently. Fix it structurally: give every test its own tenant or account, generate unique identifiers per test rather than reusing seed data, wrap each test in a transaction that rolls back where the stack allows it, and run the suite with a randomised order (pytest -p randomly, --shuffle in most runners) in CI. If shuffling turns the suite red, you did not have a passing suite, you had a lucky one.
Count the assertions in your suite that involve a status code above 399. For most teams it is under ten percent, yet clients spend their production lives in those branches. Test that a 401 and a 403 are actually distinguishable, that a 422 returns per-field validation errors in the shape your form expects, that a 409 on a duplicate create is a 409 and not a 500 with a leaked constraint name, and that a 429 includes Retry-After. If you are using the problem-details format from RFC 9457, validate the application/problem+json body against its schema too. Error responses are part of the contract; treating them as untyped strings is how a client ends up parsing English.
Every client that retries needs the server to be safe about it, and almost nobody writes the test. The test is simple: send the same POST twice with the same Idempotency-Key, assert the second returns the same resource id and that only one row exists. Then send the same key with a different body and assert it is rejected rather than silently overwriting. Add a fault injector such as Toxiproxy in front of a dependency to confirm your retry loop backs off instead of hammering, and use k6 to confirm that behaviour holds at concurrency rather than one request at a time.
The strongest cure for mock drift is to stop inventing payloads. VCR-style cassettes capture a real interaction once, replay it deterministically afterwards, and can be re-recorded on a schedule so a third-party change fails your build rather than your customers' checkout. Keep two tiers: a fast suite of recorded interactions on every push, and a small nightly job that hits the real sandbox and re-records. When the nightly job fails, the third party changed something and you now know before they announce it. That pairing, plus schema assertions, catches more real defects than doubling your unit test count. For tool-by-tool setup, see the API testing tools quick start.