How to Write Test Cases: Examples and a Copyable Template

A practical guide to writing test cases that survive review: structure, real examples, a reusable template, and the mistakes that get test cases rejected.

A test case is a contract: it tells whoever runs it exactly what to do, exactly what to expect, and leaves no room for "well, it looked fine to me". Most test cases fail at this. They say "test the login page" — which is a wish, not a test. This guide walks through the structure that holds up in real projects, three worked examples at different depths, a template you can copy today, and the review comments that come back when a test case is written badly. Everything here works whether you track cases in a test management tool, a spreadsheet, or markdown files.

The anatomy of a test case

Every usable test case has the same seven parts. Skip one and the person running your case two months from now will improvise — and improvisation is where bugs slip through:

  • ID — a stable identifier (TC-001). Tools regenerate numbers; stable IDs survive reordering and are citable in bug reports.
  • Title — one line naming the scenario: "Login rejects a password shorter than 8 characters". Not "login testing".
  • Preconditions — the state that must exist before the steps start: a user account exists, the environment is staging, a feature flag is on.
  • Steps — numbered, each step one action. If a step contains the word "and", split it.
  • Test data — the exact inputs. "Enter an invalid password" is not data; "enter Pass1" is.
  • Expected result — observable, specific, and ideally verifiable by more than one sense: the exact error message text, not "an error appears".
  • Priority — so a time-boxed regression pass knows what to run first.

If your team argues about whether to include an "actual result" field: that belongs in the bug report, not the test case. The test case states intent; the run records reality.

Worked example 1: a login validation case

Here is the anatomy applied to a real scenario. This is the level of detail that lets a new team member run the case without asking a single question:

FieldValue
IDTC-LOGIN-014
TitleLogin rejects passwords shorter than 8 characters
PreconditionsTest account qa_user@example.com exists; password policy requires 8+ chars, 1 uppercase, 1 digit
Steps1. Open /login. 2. Enter qa_user@example.com. 3. Enter Pass1. 4. Click "Sign in".
Test dataEmail: qa_user@example.com; password: Pass1 (5 chars, has uppercase + digit, too short)
Expected resultForm does not submit. Inline error under the password field reads: "Password must be at least 8 characters." Focus moves to the password field. No network request to /api/auth/login appears in dev tools.
PriorityHigh

Two details do most of the work here. The test data is exact — anyone can reproduce the case byte-for-byte. And the expected result names the message text and the absence of a network request, which distinguishes client-side validation from a round trip the server also handles. Vague cases collapse those into "error shown", and then nobody notices when the client-side check disappears in a refactor.

Worked example 2: an API test case

API cases follow the same anatomy, but preconditions carry more weight because state is explicit. The expected result should pin status code, body shape, and the side effect that proves the work happened:

FieldValue
IDTC-API-003
TitlePOST /orders with a malformed email returns 422 and creates nothing
PreconditionsValid API token for the test tenant; orders table empty for that tenant
Steps1. POST to /v1/orders with the JSON body below.
Test data{"email": "not-an-email", "items": ["sku-123"]}
Expected resultHTTP 422. Response body: {"error": "invalid_email"}. A follow-up GET /v1/orders returns zero orders for the tenant.
PriorityCritical

The follow-up GET is the part reviewers strip out first and miss most. "Returns 422" alone passes even when the API also created the order and returned an error — a real bug class. Proving the negative (nothing was created) is what makes the case worth running.

Worked example 3: a data-validation case with bad data

Form cases multiply fast. A single email field alone justifies a dozen cases: empty, whitespace, missing @, double @, unicode domain, 255 chars, 256 chars, leading/trailing spaces, SQL-looking input. You do not write all of those by hand each sprint — you write them once as a fixture set and reuse. Our guide to generating realistic test data for QA covers building that fixture library; the Email Validator spells out the acceptance rules a compliant email checker applies, which is exactly the source of truth your expected results should cite.

One case from that family, written properly:

ID:        TC-FORM-EMAIL-006
Title:     Signup rejects a 256-character email with the standard length error
Steps:     1. Open /signup
           2. Enter an email of 256 'a' characters followed by '@t.example'
           3. Fill remaining required fields with valid data
           4. Submit
Expected:  Inline error "Email must be 250 characters or fewer."
           No confirmation email is sent (check mail test inbox).
Priority:  Medium

The template

Copy this skeleton into your test management tool or repo. The comment lines are the review checklist — delete them before publishing:

ID:        TC-<AREA>-<NNN>
Title:     <System> <action> <observable outcome>
Priority:  Critical | High | Medium | Low
Preconditions:
  - <state that must exist, incl. accounts/env/flags>
Steps:
  1. <one action>
  2. <one action>
Test data:
  - <exact inputs; cite the fixture file if the set is large>
Expected result:
  - <exact observable outcome: message text, status code, side effect>
  - <the negative proof, when relevant: nothing else changed>

Where structured scenarios fit: Gherkin

If your cases feed automation, write them in Given/When/Then. The same information survives the format change — preconditions become Given, steps become When, expected result becomes Then — but you gain executable, reviewable scenarios that non-programmers can read. When you draft them, the Gherkin Validator catches syntax slips (missing keywords, bad indentation, unescaped pipes) before they reach your CI. Cucumber's official Gherkin reference is the canonical syntax documentation.

The review comments every writer gets (and how to pre-empt them)

  • "Not reproducible." Your test data says "invalid email". Fix: exact strings, or a named fixture file with a version.
  • "How do I verify this?" Your expected result says "works correctly". Fix: observable specifics — message text, status code, row count, network behavior.
  • "This tests three things." Fix: one behavior per case. A case with multiple outcomes produces ambiguous pass/fail and useless metrics.
  • "Duplicate of TC-042." Happens when titles are generic. Fix: title the scenario, not the feature, and keep stable IDs so dedup is by intent.
  • "No cleanup." API and data cases create state. Fix: note the teardown (delete the test order, revoke the token) in preconditions or a final step.

How many cases is enough?

Coverage thinking beats counting. For each requirement ask: what is the happy path, what are the boundary values, what are the invalid inputs, and what state changes does the flow cause? Write one case per answer that differs observably. That usually lands between five and fifteen cases per requirement — enough to catch real bugs, few enough to maintain. When the numbers get unwieldy, the QA Metrics Calculator computes coverage, defect density, and defect removal efficiency so you can see whether added cases are actually finding bugs or just padding the suite.

Related reading