Guides

How to Write API Test Cases: Template + Examples (2026)

Parveen KumariUpdated Aug 20, 202613 min read

Quick answer

A well-written API test case has five parts: an ID and title that name exactly what's being tested, a precondition (what state the system must be in), the request (method, endpoint, payload), the expected result (status code and response shape, not just "it works"), and a postcondition confirming no unintended side effects. Writing test cases in this template before automating them keeps coverage intentional — positive, negative, and boundary cases planned on purpose, not discovered by accident when something breaks in production.

Reviewed by Rishi Gaurav

Share:
Bar chart showing a typical breakdown of 8 to 15 test cases per endpoint across positive, negative, boundary, security, and idempotency categories

An API test case is a documented, specific scenario describing exactly what to send to an endpoint and exactly what should come back — distinct from the test automation code that later executes it. Writing the test case first, even in outline form, forces a decision about what "correct" means before you write the assertion that checks it, and produces something a non-engineer can review before any code exists.

This guide gives a reusable template, worked examples across positive, negative, boundary, and security scenarios, a naming convention, and how each written test case maps directly onto an automated test in whichever framework you use.

In this guide

  1. The API Test Case Template
  2. Naming Convention
  3. Positive Test Case Examples
  4. Negative Test Case Examples
  5. Boundary Test Case Examples
  6. Security Test Case Examples
  7. How Many Test Cases Does One Endpoint Need?
  8. From Test Case to Automated Test
  9. Common Mistakes When Writing Test Cases
  10. Deciding which cases to write first
  11. Parameterise instead of copying
  12. Writing cases someone else can review
  13. When to delete a test case
  14. Frequently asked questions about API test cases

The API Test Case Template

FieldDescription
IDA unique, sortable identifier (e.g. TC_Users_Get_001)
TitleA one-line description of exactly what's being tested
PreconditionWhat state the system must be in before this test runs
Method & EndpointThe HTTP method and path under test
Request PayloadHeaders, query params, and body sent with the request
Expected Status CodeThe exact status code expected — not "success," a number
Expected ResponseThe response shape or specific field values expected
PostconditionWhat should (or should not) have changed as a result

Every field earns its place: skipping Precondition produces a flaky test that only passes when run in a specific order; skipping Postcondition misses side effects a status-code-only check can't catch (a DELETE that returns 200 but doesn't actually delete anything).

If you need a target to practise against before pointing this at your own service, public and dummy APIs for testing lists the stable, key-free sandboxes worth using.

If you are on the other side of this — being asked about it rather than doing it — API testing interview questions covers what each round is actually assessing.

Naming Convention

A consistent pattern makes a test report scannable without opening each case individually:

TC_[Feature]_[Action]_[Condition]_[ExpectedResult]

Examples:

TC_Users_Get_ValidID_Returns200
TC_Users_Get_NonexistentID_Returns404
TC_Users_Create_MissingEmail_Returns400
TC_Users_Create_DuplicateEmail_Returns409
TC_Posts_Delete_UnauthorizedUser_Returns403

The exact format matters less than consistency across the whole suite — pick one, document it once, and every test case's purpose is readable from its name alone in a CI report.

Positive Test Case Examples

IDTitlePreconditionRequestExpected
TC_Users_Get_001Get an existing user by valid IDUser with ID 1 existsGET /users/1200, body matches user schema
TC_Posts_Create_001Create a post with valid required fieldsUser with ID 1 existsPOST /posts with {title, body, userId: 1}201, response echoes submitted fields plus a new id
TC_Posts_Update_001Update an existing post's titlePost with ID 1 existsPUT /posts/1 with updated title200, title reflects the new value

Negative Test Case Examples

IDTitlePreconditionRequestExpected
TC_Users_Get_002Get a user with a nonexistent IDUser with ID 999999 does not existGET /users/999999404, error body has consistent shape
TC_Posts_Create_002Create a post missing the required title fieldPOST /posts with {body, userId: 1} (no title)400/422, error message names the missing field
TC_Posts_Create_003Create a post with a wrong data typePOST /posts with userId: "one" (string, not integer)400/422, rejected before reaching business logic

Boundary Test Case Examples

Bugs cluster at the edges of valid ranges far more often than in the middle, which is why boundaries deserve their own dedicated cases rather than being left to chance:

IDTitlePreconditionRequestExpected
TC_Posts_List_001Request page 0 of a paginated listGET /posts?_page=0Defined behavior (either treated as page 1, or a 400) — whichever the spec documents, tested explicitly
TC_Posts_List_002Request a page number past the last pageOnly 10 pages existGET /posts?_page=999200 with an empty result array, not an error
TC_Posts_Create_004Submit a title at the maximum allowed lengthMax length is documented as 255 charsPOST /posts with a 255-char title201, accepted at exactly the limit
TC_Posts_Create_005Submit a title one character over the maximumPOST /posts with a 256-char title400/422, rejected just past the limit

Ready to shift left with your API testing?

Try our no-code API test automation platform free. Generate tests from OpenAPI, run in CI/CD, and scale quality.

Security Test Case Examples

IDTitlePreconditionRequestExpected
TC_Users_Get_003Access a resource without authenticationGET /account with no Authorization header401
TC_Users_Get_004Access another user's private resource (BOLA check)User A is authenticated; resource belongs to User BGET /users/{userB_id}/private-data as User A403 (not 200 with User B's data)
TC_Auth_Login_001Attempt login with an expired token on a protected routeToken issued more than its TTL agoGET /account with an expired Authorization header401, not a degraded but authenticated response

See our full checklist for the complete set of functional, security, and performance items these examples are drawn from, and the OWASP API Top 10 testing guide for deeper security-specific cases like these.

How Many Test Cases Does One Endpoint Need?

Enough to cover the happy path, every documented error status, key boundary values per input field, and at least one authorization case. For a typical CRUD endpoint, that's commonly 8–15 test cases:

  • 1–2 positive cases (valid input, valid ID)
  • 2–4 negative cases (missing fields, wrong types, invalid IDs)
  • 2–3 boundary cases (min/max length, empty, pagination edges)
  • 1–2 security cases (unauthenticated, unauthorized/BOLA)
  • 1–2 idempotency or side-effect cases (retry, verify postcondition)

Endpoints with more complex business logic — conditional validation, multi-step workflows, state machines — need proportionally more.

From Test Case to Automated Test

Each row in the tables above maps directly onto one automated assertion. TC_Users_Get_002 (nonexistent ID returns 404) in a few frameworks:

# pytest
def test_get_user_nonexistent_id_returns_404(api_session, base_url):
    response = api_session.get(f"{base_url}/users/999999", timeout=10)
    assert response.status_code == 404
// REST Assured
@Test
void getUserNonexistentIdReturns404() {
    given()
    .when()
        .get("/users/999999")
    .then()
        .statusCode(404);
}
# Karate
Scenario: get user with nonexistent ID returns 404
  Given path 'users/999999'
  When method get
  Then status 404

See the pytest, REST Assured, Playwright, Cypress, and Karate tutorials for the complete pattern in each framework — every one of them accepts test cases written in this template as direct input.

Common Mistakes When Writing Test Cases

  • Writing "it works" as the expected result instead of the exact status code and response shape — an ambiguous expectation produces an ambiguous automated assertion.
  • Skipping the precondition, producing a test that only passes when run after another specific test happens to have created the data it needs.
  • Testing only the happy path per endpoint and treating negative/boundary cases as optional extras rather than an equal third of the suite.
  • No postcondition check, so a DELETE that returns 200 without actually deleting anything passes undetected.
  • Inconsistent naming across the suite, making a CI failure report unreadable without opening every individual case.
  • Writing test cases that describe implementation instead of behavior — "calls the UserService" instead of "returns 200 with the user's email field," which breaks the moment the implementation is refactored even though behavior is unchanged.

Deciding which cases to write first

An endpoint can support dozens of defensible test cases and rarely deserves all of them. Ranking by risk rather than by category keeps the suite proportional to what it protects.

SignalWhy it raises priority
Money or entitlement changes handsA wrong result is not recoverable by a retry
The endpoint writes or deletesRead failures inconvenience; write failures corrupt
It enforces an authorization boundaryThe failure is a data breach, not a bug report
It has broken beforePast defects cluster; the code is complex for a reason
It has many callersBlast radius, and slower detection
The logic branches on inputBranches are where untested paths hide

Work down that list and stop when the marginal case stops encoding a requirement anyone would be paged for. A practical shape for a typical write endpoint is one happy path, two or three validation failures on the fields that matter, one authorization case per role that should be refused, and a boundary case wherever a limit exists. That is six to eight cases, not thirty.

The corollary matters as much: a read-only endpoint returning public reference data can reasonably have one test. Spending equal effort per endpoint is how suites become slow without becoming safer.

Free Guided worksheet

Build Your Testing Strategy in 30 Minutes

A structured worksheet that walks you through defining your testing strategy in 30 minutes. Cover architecture, tools, layers, and team responsibilities.

Download Free

Parameterise instead of copying

Once you have identified the boundaries, writing one case per value produces near-identical blocks that drift apart as they are edited. Table-driven cases keep the intent in one place:

import pytest

@pytest.mark.parametrize("qty,expected_status,reason", [
    (1,      201, "minimum valid quantity"),
    (99,     201, "maximum valid quantity"),
    (0,      422, "below minimum"),
    (100,    422, "above maximum"),
    (-1,     422, "negative"),
    ("two",  422, "wrong type"),
])
def test_order_quantity_boundaries(client, qty, expected_status, reason):
    response = client.post("/v1/orders", json={"sku": "A-1", "qty": qty})
    assert response.status_code == expected_status, f"{reason}: got {response.text}"

Two things this buys. Adding a boundary is one line rather than one function, so people actually add them. And the failure message names the reason — "above maximum: got 201" tells a reviewer what broke without opening the file.

The limit to watch: parameterisation is for cases that differ only in data. When cases differ in setup or in what they assert, separate functions stay clearer than a table with conditionals in it.

Writing cases someone else can review

A test case is read far more often than it is written — during review, during triage, and by whoever inherits it. Four habits make that reading cheap.

Name the behaviour, not the mechanics. test_order_rejected_when_quantity_below_minimum tells a reviewer what the system should do. test_post_orders_2 tells them nothing and hides whether the case duplicates another.

Make setup obvious at the top. A reviewer should see what state the test assumes without following three layers of fixtures. Shared fixtures are good; hidden ones are not.

Assert one behaviour per case. A test asserting six unrelated things fails on the first and hides the rest, and its name cannot honestly describe it.

Say why, when why is not obvious. A case that exists because of a past incident should say so in one line. Otherwise someone eventually deletes it as redundant, and the incident recurs.

When to delete a test case

Suites are pruned far less often than they are grown, and an unpruned suite loses trust slowly. Four cases are worth removing rather than fixing.

It asserts an implementation detail. A test that breaks on every safe refactor is measuring structure rather than behaviour. It costs maintenance and catches nothing.

It duplicates a case at a cheaper layer. An end-to-end test verifying a validation rule already covered by a unit test adds minutes to every run for no additional signal.

It has been skipped for more than a release. A skipped test is documentation of an intention. If nobody has returned to it, delete it and file the gap where it will actually be seen.

It cannot fail. Tests that assert something the framework or the type system already guarantees pass permanently, which reads as coverage and is not.

Everything else — including tests that fail sometimes — should be fixed rather than removed. A flaky test is reporting a real problem, usually in setup or in an assumption about ordering.

Frequently asked questions about API test cases

What are the essential parts of an API test case? An ID and title, a precondition, the request itself, the expected result (status code and response shape), and a postcondition confirming no unintended side effects.

What is the difference between positive and negative test cases? A positive case sends valid input and expects success. A negative case deliberately sends invalid input and expects the API to reject it correctly.

What are boundary test cases in API testing? Cases targeting the edges of valid input ranges — minimums, maximums, empty values, and pagination edges — where bugs cluster more often than in the middle of a valid range.

How do I name API test cases consistently? A common pattern is TC_[Feature]_[Action]_[Condition]_[ExpectedResult]. The exact format matters less than applying it consistently across the suite.

How many test cases does one API endpoint need? Commonly 8–15 for a typical CRUD endpoint: the happy path, documented error statuses, boundary values, and at least one authorization case.

Should I write test cases before or after building the automated tests? Before, at least in outline — it forces a decision about what "correct" means before you write the assertion, and produces a reviewable artifact.

Sources and further reading

Key takeaways

  • Precondition and postcondition are the most commonly skipped template fields — and the ones that catch flaky ordering bugs and silent side effects.
  • "It works" is not an expected result. Write the exact status code and response shape.
  • Positive, negative, and boundary cases are roughly equal thirds of a complete suite, not happy-path-plus-afterthoughts.
  • A consistent naming convention makes CI failure reports scannable without opening every case.
  • 8–15 test cases per typical CRUD endpoint is a reasonable baseline; complex business logic needs more.
  • Every written test case maps directly onto one automated assertion, regardless of which framework executes it.

Generate Test Cases Automatically from Your OpenAPI Spec

Writing 8-15 test cases per endpoint by hand scales linearly with your API's size. Total Shift Left generates positive, negative, and boundary test cases directly from your OpenAPI spec — the same structure as the template above, covering every endpoint automatically and regenerating when the spec changes.

Start your free trial to see generated test cases for your own API, or see plans and pricing if you're already evaluating.

Ready to shift left with your API testing?

Try our no-code API test automation platform free.