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#

typescript
// 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#

typescript
// 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().

Full explanation →

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.

Full explanation →

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.

Full explanation →

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.

Full explanation →

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.

Full explanation →

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.

Full explanation →

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.

Full explanation →

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.

Full explanation →

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.

Full explanation →

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.

Full explanation →

Official references and related topics#

Optional online further reading; all core study material is included above.

Back to library index

Prepared for both Senior automation roles. Hypothetical sandbox contracts; no employer systems or credentials. Content: 16 October 2026.