P0 · Chapter 04 / 15
API / HTTP testing
The 60-second cheat sheet#
An API test validates a transport contract and business effects at a service boundary. HTTP success is evidence of request handling, not necessarily final workflow success. Read the OpenAPI/requirements, assert the specified status and runtime body, check permissions and side effects, then clean owned test data.
HTTP decision tables#
| Method | Intended semantics | Test implication |
|---|---|---|
| GET | Read; safe and idempotent intended semantics | Must not mutate business state; caching may matter |
| POST | Submit/create/process, generally not idempotent | Duplicate/replay behavior needs explicit contract |
| PUT | Replace target representation; intended idempotent | Repeat and verify effect, not identical timestamps |
| PATCH | Partial modification | Idempotency depends on patch operation (set vs increment) |
| DELETE | Remove target; intended idempotent effect | Second status may differ without violating semantics |
| Status | Typical meaning / assertion |
|---|---|
| 200 / 201 / 202 / 204 | OK / created / accepted for processing / no body; use contract-specific exact code |
| 400 / 422 | Malformed request / semantically invalid input where contract distinguishes |
| 401 / 403 | Missing/invalid authentication / insufficient permission; security policy can conceal resources |
| 404 / 409 | Not found / conflict such as duplicate or invalid transition |
| 429 | Rate limit; inspect Retry-After if specified |
| 500 / 502 / 503 | Internal failure / gateway upstream error / service unavailable |
Authentication establishes identity, authorization decides permitted actions. A valid token for user A must not authorize reading B's records. Test object-level permissions, not just “token present.” Error messages must not leak secrets or internal details.
Anatomy, schema and test design#
Method + path + query + headers + body describe a request. A path ID identifies a resource; query parameters filter/paginate. Validate Content-Type before assuming JSON, required fields/types, business invariants and error shape. A TypeScript DTO describes expectations only; optional Zod/Ajv can validate runtime schema but are not dependencies of this website.
Cursor pagination needs stable ordering and no duplicates/gaps across pages; offset pagination can shift under concurrent writes. At awareness level, ETag + If-None-Match supports conditional reads, while If-Match can prevent lost updates if the API implements it. Verify caching does not expose private cross-user data.
| Risk | Useful tests |
|---|---|
| Input | Required/absent/null/wrong type, min/max, Unicode, format, oversized payload |
| Identity | No token, expired token, wrong role, another user's resource |
| State | Duplicate create, invalid transition, concurrent updates, stale version |
| Reliability | Timeout after commit, 429, dependency 503, malformed response |
| Side effects | Exactly one ledger/event/audit effect per accepted operation under contract |
POST → GET with cleanup#
// tests/api/users.spec.ts: complete test; hypothetical authorized test APIs.
import { test, expect } from '@playwright/test';
import { randomUUID } from 'node:crypto';
test('created user can be fetched', async ({ request }, testInfo) => {
const correlation = randomUUID();
const created = await request.post('/test-support/users', {
data: { name: `QA-${correlation}`, correlation },
});
expect(created.status()).toBe(201);
const data: unknown = await created.json();
if (!data || typeof data !== 'object' || !('id' in data) || typeof data.id !== 'string') {
throw new Error('Creation did not return a string id');
}
const id = encodeURIComponent(data.id);
let primaryFailed = false;
try {
const fetched = await request.get(`/api/users/${id}`);
expect(fetched.status()).toBe(200);
expect(await fetched.json()).toMatchObject({ id: data.id, name: `QA-${correlation}` });
} catch (error) { primaryFailed = true; throw error; }
finally {
try {
const deleted = await request.delete(`/test-support/users/${id}`);
expect(deleted.status()).toBe(204);
} catch (error) {
await testInfo.attach('cleanup-failure', { body: String(error), contentType: 'text/plain' });
if (!primaryFailed) throw error;
}
}
});
Assumes configured baseURL/auth and a safe test-support contract. Cleanup covers failures after ID parsing. If creation commits but response/JSON is lost, use correlation lookup or supported idempotency to find the record; finally cannot clean an unknown ID. A cleanup API outage remains an outstanding resource leak: record it and use an authorized TTL janitor.
Direct calls, negatives and browser mocks#
// Scoped test body with request + expect. Illustrative contracts.
const invalid = await request.post('/api/users', { data: { email: 'invalid' } });
expect(invalid.status()).toBe(422);
expect(await invalid.json()).toMatchObject({ code: 'INVALID_EMAIL' });
const changed = await request.put('/api/profiles/owned-qa-user', { data: { displayName: 'QA' } });
expect(changed.status()).toBe(200);
const missing = await request.get('/api/users/nonexistent-qa-id');
expect(missing.status()).toBe(404);
.ok() accepts any 2xx; await expect(response).toBeOK() also checks success status, not the business body. A 202 PENDING is not VERIFIED. request.get/post/put/delete are real HTTP through APIRequestContext; page.route intercepts browser traffic only. API-first setup reduces cost; UI login/create tests remain when those interactions carry business risk. Postman is effective for exploration and sharing examples, Newman for collection CI; Playwright integrates typed fixtures, UI/API workflows and runner evidence. Neither tool substitutes for useful assertions.
Retry, concurrency and payments#
A client timeout means “outcome unknown,” not “server did nothing.” Before replaying a mutation, query by correlation/payment ID. An idempotency key helps only if the backend stores and enforces the key, payload consistency and deduplication window. In a sandbox, submit the same supported key twice and verify the same payment identity and one ledger effect; also test changed payload with same key and window expiry according to contract. Never replay real financial writes blindly.
Concurrent GETs can use Promise.all. Concurrent writes require a designed collision test, isolated resources and expected conflict behavior; do not accidentally create one through shared fixtures. Honor bounded retry policies only for classified transient failures. A returned HTTP 503 still needs status assertion; Promise rejection handling alone is insufficient.
Contract testing and traceability#
OpenAPI checks validate documented shape/status but may miss actual business semantics or compatibility expectations. Consumer-driven contracts record the interaction a consumer needs and verify providers satisfy it; they do not replace deployed integration checks. Trace Jira acceptance criteria → TestRail case/suite → automated test identifier → CI run/results → defect. TestRail runs execute selected cases; plans organize runs/configurations; reports support evidence. Vendor fields and integrations vary.
Wrong versus right and interview response#
| Wrong | Right | Why |
|---|---|---|
Expect .ok() only |
Status + body + effect | 2xx may represent wrong business state |
| Hard-code created ID | Use returned validated ID | Independent parallel tests |
| Retry payment on timeout | Reconcile then supported safe replay | Avoid duplicate financial effects |
| Cast JSON to DTO | Runtime validation | Static types don't inspect bytes |
| Check 401 and call authorization covered | Test cross-user/role 403 policy | Identity and permissions differ |
30 seconds: “I derive API tests from the real contract: exact statuses, runtime schema, business rules and side effects. I cover negative and authorization paths, create unique data and fetch by returned IDs. For async acceptance I check final completion separately. I treat uncertain mutation outcomes as reconciliation problems before retrying.”
90-second senior answer: designing an API suite
I start with critical workflows and boundary contracts, then partition validation into positive, boundary, permission, concurrency and dependency-failure scenarios. I use service-level tests for most business combinations and retain a small number of UI/API integration paths. Each test owns its records, uses correlation IDs and cleans up in a fixture or finally block. I validate status, content type, shape and domain invariants; HTTP 201 and VERIFIED are different dimensions. For pagination I check completeness under the documented ordering. For a timeout after payment submission I query the backend and use only contract-supported idempotency. Reports link to acceptance criteria and include sanitized diagnostics. I distinguish a provider defect from test-data or environment failures before deciding whether a quality gate can be relaxed.
Memory hook: method + identity + shape + state + effect + cleanup.
Quick practice · reveal answers#
What is the API testing checklist?
- Assert exact status, runtime body and business effects.
- Cover boundaries, permissions, invalid state, concurrency and cleanup.
Pitfall: Only checking response.ok().
401 versus 403?
- 401 usually means absent/invalid authentication; 403 means authenticated/identified but forbidden under policy.
- Check resource-concealment rules.
Pitfall: Testing only token presence and calling authorization covered.
Is POST idempotent?
- Not inherently.
- An enforced backend idempotency contract can make a particular submission safely repeatable.
Pitfall: Assuming a header name alone prevents duplicates.
How do you test POST then GET?
- Await creation, validate returned ID, fetch that resource and assert contents
- own cleanup.
Pitfall: Hard-coded IDs or concurrent dependent calls.
What if a payment request times out?
- Outcome is unknown.
- Query by operation/correlation identity before replay; use only documented enforced idempotency in sandbox.
Pitfall: Assuming timeout means no charge happened.
What does HTTP 202 prove?
- Acceptance according to contract, not final outcome.
- Poll the operation/status endpoint to a terminal business state.
Pitfall: Equating HTTP code with VERIFIED status field.
How do you test Kafka duplicates?
- Send controlled duplicate business/event identity and verify one logical DB/audit/ledger effect.
- Include restart boundary when relevant.
Pitfall: Assuming delivery semantics guarantee all external effects once.
Is Kafka globally ordered?
- Ordering depends on partition/key and configuration.
- Specify the exact ordering/version contract and test out-of-order handling.
Pitfall: Expecting global order across unrelated partitions.
How do you reconcile source and target?
- Compare keys both directions
- duplicates, values and compatible grouped totals with a stable watermark/snapshot.
Pitfall: Equal counts imply identical records.
An assertion fails before delete runs: what changes?
- Move owned cleanup into finally/fixture teardown
- preserve original and cleanup diagnostics.
Pitfall: Assuming cleanup succeeds whenever finally executes.
Official references and related topics#
Optional online further reading; all core study material is included above.