On this page12 sections
ReferenceBeginner8-10 min reference

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:

FieldWhat it capturesExample
IDStable identifier for trackingTC-LOGIN-001
TitleOne-sentence summaryVerify successful login redirects to dashboard
PreconditionsWhat must be true before steps runUser account exists; user is logged out
StepsNumbered, deterministic actions1. Navigate to /login. 2. Enter valid credentials…
Expected ResultThe observable outcomeUser is redirected to /dashboard within 2 seconds
Actual ResultWhat happened during executionFilled in at run time
StatusPass / Fail / Blocked / SkippedPass

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:

FieldValue
IDTC-LOGIN-001
TitleVerify successful login with valid credentials redirects to dashboard
ModuleAuthentication
PriorityP0
PreconditionsActive user account qa.user@example.com exists with password Secret!23
Test DataEmail: qa.user@example.com / Password: Secret!23
Steps1. Navigate to /login
2. Enter email in the Email field
3. Enter password in the Password field
4. Click the Sign In button
Expected Result1. 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:

FieldValue
IDTC-LOGIN-007
TitleVerify login fails with clear error message when password is incorrect
ModuleAuthentication
PriorityP1
PreconditionsActive user account exists
Test DataEmail: qa.user@example.com / Password: WrongPassword
Steps1. Navigate to /login
2. Enter valid email and incorrect password
3. Click Sign In
Expected Result1. User remains on /login
2. 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:

FieldValue
IDTC-SIGNUP-PWD-BOUNDARY-001
TitleVerify password length validation at boundaries (7, 8, 16, 17 characters)
ModuleSign-up
PriorityP1
PreconditionsOn /signup, all other fields are valid
Test Data7-char: Pass!23
8-char: Pass!234
16-char: Pass!23456789012
17-char: Pass!234567890123
StepsFor each value: enter in Password field and click Submit
Expected Result7 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):

PartitionRangeSample valueExpected
Invalid (negative)<0-5Reject
Invalid (too young)0–1712Reject
Valid18–12035Accept
Invalid (too old)121+200Reject
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):

BoundaryValues to test
Lower edge17 (invalid), 18 (valid), 19 (valid)
Upper edge119 (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
#MemberCart ≥ $100Promo validDiscount applied
1yesyesyes25% (member + bulk + promo)
2yesyesno15% (member + bulk)
3yesnoyes15% (member + promo)
4yesnono5% (member only)
5noyesyes20% (bulk + promo)
6noyesno10% (bulk only)
7nonoyes10% (promo only)
8nonono0%

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:

FromToTriggerAllowed?
PendingPaidPayment succeeds
PendingCancelledCustomer cancels
PaidShippedWarehouse dispatches
PaidRefundedPayment reversed
ShippedDeliveredCarrier confirms
DeliveredPendingillegal
CancelledPaidillegal

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 casesnull, 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 /dashboard and the heading 'Welcome back' is visible within 2 seconds"

Steps that combine multiple actions.

  • ❌ "Fill in the form and submit"
  • ✓ "Enter qa@example.com in Email. Enter Secret!23 in 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.