Test Case Writing
A test case is a contract between QA, engineering, and the product team. A good one tells anyone — including future you — what to verify, how to verify it, and what counts as success. This sheet covers the structure, naming, design techniques, and templates worth reaching for.
Anatomy of a good test case
Every test case answers six questions:
| Field | What it captures | Example |
|---|---|---|
| ID | Stable identifier for tracking | TC-LOGIN-001 |
| Title | One-sentence summary | Verify successful login redirects to dashboard |
| Preconditions | What must be true before steps run | User account exists; user is logged out |
| Steps | Numbered, deterministic actions | 1. Navigate to /login. 2. Enter valid credentials… |
| Expected Result | The observable outcome | User is redirected to /dashboard within 2 seconds |
| Actual Result | What happened during execution | Filled in at run time |
| Status | Pass / Fail / Blocked / Skipped | Pass |
If a test case is missing any of the first five, it can't be reliably executed by someone else. That's the bar.
Writing effective titles
A title should let a triager decide in two seconds whether to read the rest. Use the pattern:
Verify [action] [expected outcome] when [condition]
Good titles:
Verify checkout completes when promo code is valid and cart is non-empty
Verify password reset email is sent within 30 seconds when user submits valid email
Verify Submit button is disabled when required fields are empty
Weak titles to avoid:
Test login ← what about login?
Login should work ← define "work"
Verify error ← which error, on what input?
Naming conventions
Use a stable, sortable, hierarchical ID. The convention many teams settle on:
TC-[Module]-[Feature]-[Number]
TC-AUTH-LOGIN-001
TC-AUTH-LOGIN-002
TC-AUTH-PASSWORD-RESET-001
TC-CHECKOUT-PROMO-CODE-001
TC-API-ORDERS-CREATE-001
Keep numbers zero-padded so alphabetical sort matches numeric sort. Don't recycle IDs — once a test case exists, retire it rather than reuse the number.
Functional test case template
For happy-path verification:
| Field | Value |
|---|---|
| ID | TC-LOGIN-001 |
| Title | Verify successful login with valid credentials redirects to dashboard |
| Module | Authentication |
| Priority | P0 |
| Preconditions | Active user account qa.user@example.com exists with password Secret!23 |
| Test Data | Email: qa.user@example.com / Password: Secret!23 |
| Steps | 1. Navigate to /login2. Enter email in the Email field 3. Enter password in the Password field 4. Click the Sign In button |
| Expected Result | 1. Login form is displayed 2. Email accepts input 3. Password is masked 4. User redirected to /dashboard and dashboard heading "Welcome back" is visible within 2 seconds |
| Actual Result | (filled at run time) |
| Status | (filled at run time) |
Negative test case template
For invalid input and error paths:
| Field | Value |
|---|---|
| ID | TC-LOGIN-007 |
| Title | Verify login fails with clear error message when password is incorrect |
| Module | Authentication |
| Priority | P1 |
| Preconditions | Active user account exists |
| Test Data | Email: qa.user@example.com / Password: WrongPassword |
| Steps | 1. Navigate to /login2. Enter valid email and incorrect password 3. Click Sign In |
| Expected Result | 1. User remains on /login2. Inline error displays: "Email or password is incorrect" 3. Password field is cleared 4. Email field retains the entered value 5. No info leak about whether the email exists |
| Actual Result | (filled at run time) |
| Status | (filled at run time) |
Boundary value test case template
For inputs with numeric or length limits — e.g., password requires 8–16 characters:
| Field | Value |
|---|---|
| ID | TC-SIGNUP-PWD-BOUNDARY-001 |
| Title | Verify password length validation at boundaries (7, 8, 16, 17 characters) |
| Module | Sign-up |
| Priority | P1 |
| Preconditions | On /signup, all other fields are valid |
| Test Data | 7-char: Pass!238-char: Pass!23416-char: Pass!2345678901217-char: Pass!234567890123 |
| Steps | For each value: enter in Password field and click Submit |
| Expected Result | 7 chars → error "Password must be at least 8 characters" 8 chars → accepted 16 chars → accepted 17 chars → error "Password must be at most 16 characters" |
| Actual Result | (filled at run time) |
| Status | (filled at run time) |
Equivalence partitioning
Group inputs into classes where the system should behave identically, then test one value per class. You don't need to test every number from 0 to 100 — you test one from each partition.
Example — age field validation (must be 18–120):
| Partition | Range | Sample value | Expected |
|---|---|---|---|
| Invalid (negative) | <0 | -5 | Reject |
| Invalid (too young) | 0–17 | 12 | Reject |
| Valid | 18–120 | 35 | Accept |
| Invalid (too old) | 121+ | 200 | Reject |
| Invalid (non-numeric) | n/a | "abc" | Reject |
| Invalid (empty) | n/a | "" | Reject |
Six test cases instead of testing every integer. Each represents an entire class of behaviour.
Boundary value analysis
Bugs cluster at the edges. After equivalence partitioning, add tests for the values immediately on either side of every boundary.
Same age field (18–120 valid):
| Boundary | Values to test |
|---|---|
| Lower edge | 17 (invalid), 18 (valid), 19 (valid) |
| Upper edge | 119 (valid), 120 (valid), 121 (invalid) |
These six values catch off-by-one mistakes the equivalence-partition tests will miss. Always pair the two techniques.
Decision table testing
When the output depends on a combination of conditions, draw the truth table — every condition × every action — and test each row.
Example — discount eligibility:
- Member: yes / no
- Cart total ≥ $100: yes / no
- Promo code valid: yes / no
| # | Member | Cart ≥ $100 | Promo valid | Discount applied |
|---|---|---|---|---|
| 1 | yes | yes | yes | 25% (member + bulk + promo) |
| 2 | yes | yes | no | 15% (member + bulk) |
| 3 | yes | no | yes | 15% (member + promo) |
| 4 | yes | no | no | 5% (member only) |
| 5 | no | yes | yes | 20% (bulk + promo) |
| 6 | no | yes | no | 10% (bulk only) |
| 7 | no | no | yes | 10% (promo only) |
| 8 | no | no | no | 0% |
Eight test cases, full coverage of the rule combinations. Decision tables make rule conflicts and missing rules visible — that table review often uncovers spec gaps before any code is written.
State transition testing
When the system has explicit states (order pending → paid → shipped → delivered, with a possible cancellation), test every legal transition and verify illegal ones are blocked.
Example — order lifecycle:
| From | To | Trigger | Allowed? |
|---|---|---|---|
| Pending | Paid | Payment succeeds | ✓ |
| Pending | Cancelled | Customer cancels | ✓ |
| Paid | Shipped | Warehouse dispatches | ✓ |
| Paid | Refunded | Payment reversed | ✓ |
| Shipped | Delivered | Carrier confirms | ✓ |
| Delivered | Pending | illegal | ✗ |
| Cancelled | Paid | illegal | ✗ |
Each row is a test case. Add edge cases like "what if the same trigger fires twice in quick succession" — those reveal idempotency bugs.
Coverage checklist
Before signing off on a feature's test suite, verify each category has at least one test:
- Positive — the happy path with clean, valid inputs.
- Negative — invalid inputs (empty, malformed, wrong type, too long, too short).
- Boundary — values at and immediately around every limit.
- Edge cases —
null,undefined, zero, leading/trailing whitespace, Unicode, RTL text, very long strings. - Auth states — logged out, expired session, wrong role, deactivated account.
- Network failures — timeout, 5xx response, offline, slow connection (3G throttling).
- Concurrency — two users editing the same record, double-click submit, back button after submit.
- Security — XSS in text inputs, SQL injection patterns, CSRF on state-changing endpoints, IDOR on URLs with IDs.
- Performance — page load under expected concurrency, response time at 95th percentile.
- Accessibility — keyboard-only navigation, screen reader labels, colour contrast, focus order.
Common mistakes to avoid
Vague expected results.
- ❌ "User should see the dashboard"
- ✓ "User is redirected to
/dashboardand the heading 'Welcome back' is visible within 2 seconds"
Steps that combine multiple actions.
- ❌ "Fill in the form and submit"
- ✓ "Enter
qa@example.comin Email. EnterSecret!23in Password. Click Sign In."
Chaining test cases.
- ❌ Test 2 starts where test 1 left off — if test 1 fails, test 2 is meaningless.
- ✓ Each test sets up its own preconditions and tears them down.
Hidden assumptions.
- ❌ "User logs in" with no specified credentials, environment, or app version.
- ✓ Preconditions list every assumption; test data is explicit.
Missing the "why".
- ❌ A test case with no link to a requirement, ticket, or user story.
- ✓ Each test references the acceptance criteria or bug it covers — so when the requirement changes, the affected tests are findable.
Treating every test as equal priority.
- ❌ 800 test cases all marked P1.
- ✓ A real prioritisation: P0 = blocks release, P1 = ship-blocker if broken in flagship flows, P2 = nice to verify, P3 = exploratory.