API testing
An HTTP API is a contract you cannot redeploy in lockstep with its clients.
When the status code, the JSON shape, or the 401-versus-403 distinction
drifts, every caller breaks at once and you learn about it from their
incident reports. Testing at the boundary means sending real requests over
HTTP and asserting the whole response, so a contract change fails your build
instead of a customer's.
Method
- Assert the whole response, not just the happy body. Check status code,
key headers (
Content-Type, Location, Cache-Control), and the body
together. A 200 with correct JSON but a missing Content-Type: application/json still breaks strict clients.
- Validate the body against a schema, not field by field. Pin the payload
to a JSON Schema, an OpenAPI component, or a pydantic model and validate
against it. This catches an added null field or a widened type that ad hoc
assert body["id"] == 5 never sees.
- Test error responses as hard as success. Drive 400, 401, 403, 404, 409,
and 422 deliberately: malformed JSON, missing field, duplicate key, unknown
id. Assert the error body shape (code, message, field) so clients can branch
on it, not just the status.
- Walk every auth path. Send no token, an expired token, a valid token
with the wrong scope, and a valid token. Assert 401 for absent or bad
credentials and 403 for authenticated-but-unauthorized: collapsing the two
leaks whether a resource even exists.
- Hit the real HTTP boundary. Use httpx, requests, supertest, or the
framework's test client against a running app so routing, middleware,
content negotiation, and serialization all run. Calling the handler function
directly skips exactly the layers that break.
- Pin the contract against the published spec. Validate responses against
the checked-in OpenAPI document, or run consumer contract tests with Pact,
so a schema change that no test named still fails the build before it ships.
Checks
- Would a widened type or an extra field in the response body fail a test?
- Does an unauthenticated request get 401 and an unauthorized one 403, each
asserted separately?
- Do malformed and conflicting requests return the documented error shape, not
a bare 500?
Boundaries
This covers one service at its HTTP edge. Cross-service journeys belong to
integration-testing, full user flows to e2e-testing, and the persistence layer
beneath the handler to database-testing. Follow the project's existing client
and schema tooling rather than adding a second assertion style.
1---2name: api-testing3description: Test an HTTP API at its boundary so status codes, response schemas, error bodies, and auth paths all hold under real requests. Use when adding or reviewing tests for a service that other clients call over HTTP.4---56# API testing78An HTTP API is a contract you cannot redeploy in lockstep with its clients.9When the status code, the JSON shape, or the 401-versus-403 distinction10drifts, every caller breaks at once and you learn about it from their11incident reports. Testing at the boundary means sending real requests over12HTTP and asserting the whole response, so a contract change fails your build13instead of a customer's.1415## Method16171. **Assert the whole response, not just the happy body.** Check status code,18 key headers (`Content-Type`, `Location`, `Cache-Control`), and the body19 together. A 200 with correct JSON but a missing `Content-Type:20 application/json` still breaks strict clients.212. **Validate the body against a schema, not field by field.** Pin the payload22 to a JSON Schema, an OpenAPI component, or a pydantic model and validate23 against it. This catches an added null field or a widened type that ad hoc24 `assert body["id"] == 5` never sees.253. **Test error responses as hard as success.** Drive 400, 401, 403, 404, 409,26 and 422 deliberately: malformed JSON, missing field, duplicate key, unknown27 id. Assert the error body shape (code, message, field) so clients can branch28 on it, not just the status.294. **Walk every auth path.** Send no token, an expired token, a valid token30 with the wrong scope, and a valid token. Assert 401 for absent or bad31 credentials and 403 for authenticated-but-unauthorized: collapsing the two32 leaks whether a resource even exists.335. **Hit the real HTTP boundary.** Use httpx, requests, supertest, or the34 framework's test client against a running app so routing, middleware,35 content negotiation, and serialization all run. Calling the handler function36 directly skips exactly the layers that break.376. **Pin the contract against the published spec.** Validate responses against38 the checked-in OpenAPI document, or run consumer contract tests with Pact,39 so a schema change that no test named still fails the build before it ships.4041## Checks4243- Would a widened type or an extra field in the response body fail a test?44- Does an unauthenticated request get 401 and an unauthorized one 403, each45 asserted separately?46- Do malformed and conflicting requests return the documented error shape, not47 a bare 500?4849## Boundaries5051This covers one service at its HTTP edge. Cross-service journeys belong to52integration-testing, full user flows to e2e-testing, and the persistence layer53beneath the handler to database-testing. Follow the project's existing client54and schema tooling rather than adding a second assertion style.