Designing API Test Cases

9 min read

The hardest part of API testing isn't the tooling — it's deciding what to test. Stare at an endpoint with five parameters and your brain offers two options: "test the obvious thing" or "test everything I can think of." Neither scales. This lesson teaches a systematic approach: read the contract, partition the inputs, enumerate the scenarios, and pick a small set of tests that gives you broad coverage without redundancy. Done well, this turns a vague "let me test the API" into a defensible test plan you can hand a teammate.

What an API test case actually is

A good API test case has four parts:

  • Endpoint — verb plus URL (POST /api/users).
  • Input — headers, query parameters, path parameters, request body.
  • Expected output — status code, response shape, key field values, side effects.
  • Preconditions — the state the system must be in before the test runs (auth token, existing user, empty cart).

Anything less and the test is ambiguous. Anything more and you're describing implementation, not behaviour.

A minimum viable template:

Test ID:        TC-API-001
Endpoint:       POST /api/users
Headers:        Content-Type: application/json
                Authorization: Bearer {valid-admin-token}
Body:           {"name": "Alice", "email": "alice@test.com", "role": "admin"}
Expected:       201 Created
                Body contains: id, name, email, role, createdAt
                Side effect: a welcome email is queued
Preconditions:  email "alice@test.com" does not already exist

Read that and you know exactly what to send and what to assert. Hand it to a colleague and they can implement it without asking you a single question.

A systematic approach

Faced with a fresh endpoint, work through five steps in order:

  1. Read the contract. OpenAPI spec, internal docs, the endpoint's controller code if all else fails. Note required fields, types, validation rules, and documented error codes.
  2. Identify the dimensions. What inputs vary? Required vs optional fields, ranges, enums, types. Each of these is a dimension your tests must cover.
  3. Apply equivalence partitioning. For each dimension, split values into classes — one valid range, one or more invalid ranges. You only need one test per class.
  4. Apply boundary value analysis. For each numeric or length-bounded field, test min, max, just-under, just-over.
  5. Enumerate scenario categories. Happy path, error paths, edge cases, security, authentication, idempotency. Make sure you have at least one test in each.

The result is a matrix where each row is a scenario type and each column is a dimension. You don't fill every cell — that explodes combinatorially — but you ensure every scenario type has a representative.

A worked example: POST /api/users

Imagine the spec says:

  • name — required string, 2-50 characters.
  • email — required, valid email format, unique.
  • role — required, one of admin, tester, viewer.
  • Returns 201 Created with the new user, or 400/409/422 on errors.
  • Requires Authorization: Bearer <admin-token>.

A test matrix you can defend in a code review:

Four rows is a starting set, not a complete one. From there, layer in:

  • Boundary cases: name with 1 char (just under min), 2 chars (min), 50 chars (max), 51 chars (just over).
  • Type errors: role: 123 (number instead of string), name: null.
  • Enum errors: role: "owner" (not in the allowed set) → 422.
  • Email format errors: not-an-email, alice@, @test.com, alice@test. Each should hit the same validation but you may want at least one of each.
  • Authorization (vs authentication) errors: a viewer token trying to create an admin → 403.
  • Edge cases: empty body {} → 400; very large name (10,000 chars) → 400 or truncation; emoji and unicode names → handled or explicit error.
  • Security cases: an SQL-like name '; DROP TABLE users;-- should be stored or rejected as a string, not executed.

You won't write every one of these as a separate test. Group equivalent inputs (all bad email formats become one test) and pick a representative from each class.

Equivalence partitioning, applied

For each input field, ask: what classes of values does this API treat the same way?

  • name length: < 2 (rejected), 2-50 (accepted), > 50 (rejected). Three classes — three tests, picking a representative from each.
  • role value: admin/tester/viewer (accepted), anything else (rejected). Two classes, but it's worth testing each accepted role since they may map to different permissions downstream.
  • email format: valid (accepted), various invalid forms (rejected). One representative valid, one or two representative invalid.

Equivalence partitioning is the technique that stops the test count exploding. Without it, you'd write hundreds of tests. With it, a dozen well-chosen tests cover the same risk.

Organising your tests

Once you have a list, organise it. Two structures work well:

  • By endpoint, then scenario type. All tests for POST /api/users live together, grouped into "happy", "validation", "auth", "edge". Easy to find and run.
  • By scenario type, then endpoint. All "auth" tests across all endpoints live together. Useful for cross-cutting smoke runs.

Most teams pick the first structure for primary organisation and use tags or filters to slice across endpoints when needed.

Test data — keep it independent

The fragile pattern: every test depends on user 42 existing in the DB. The first test deletes user 42 and the next twenty fail.

The robust pattern: each test creates its own data, asserts, and cleans up. Tests run in any order, in parallel, without coupling. This is the single most valuable habit in API test design — and we cover it in depth in Chapter 8.

For now, treat it as a rule: a test that depends on someone else's data is a test waiting to fail.

⚠️ Common mistakes

  • Testing only the happy path. "It works for valid input" tells you almost nothing about how the API handles real users, who routinely send wrong data.
  • Writing one giant test that does ten things. Each scenario deserves its own test. When test #7 fails on assertion #8, you want to know exactly which scenario broke — not have to read 200 lines of setup.
  • Ignoring the docs and reverse-engineering from the implementation. If your tests match the implementation but not the docs, the docs are wrong, the tests are wrong, or both — and consumers reading the docs will fail in unexpected ways.

🎯 Practice task

Design a full test plan. 30 minutes.

  1. Pick a real endpoint with multiple inputs — Stripe's POST /charges, JSONPlaceholder's POST /posts, or your own product's POST /something. Find or write down its contract: required fields, types, validation rules, expected status codes.
  2. List the dimensions: each input field, each header, each preconditioned state.
  3. For each dimension, list the equivalence classes (valid, invalid, boundary).
  4. Build a scenario matrix like the one above. Aim for 8-15 distinct scenarios. Categorise them: happy path, validation, conflict, auth, edge, security.
  5. Pick three of the scenarios and write them out fully using the test case template at the top of the lesson — endpoint, headers, body, expected, preconditions.
  6. Stretch: browse the API Testing Concepts cheat sheet and the HTTP Status Codes cheat sheet. Spot at least one scenario you missed (often around 405, 409, or 422).

You can now take a fresh endpoint and produce a defensible test plan. The next lesson zooms in on the trickiest scenarios — the negative and edge cases that uncover most bugs.

// tip to track lessons you complete and pick up where you left off across devices.