02 · Test Case Design & Documentation¶
A test case is the unit of work in testing. Written well, anyone on the team can execute it and get the same result. Written badly, it's a note-to-self that becomes worthless the moment you leave the project.
This module covers the anatomy of a test case, how to derive cases from requirements, and how to prove coverage with a traceability matrix.
1. Scenario vs case vs script¶
These three sit at different altitudes and get confused constantly.
| Level | Answers | Granularity | Example |
|---|---|---|---|
| Test scenario | What to test | One line, no steps | "Verify user can log in" |
| Test case | How to test it, and what "pass" means | Steps + expected result | "Login with valid credentials", 5 steps, expected: dashboard loads |
| Test script | Automated execution of a case | Code | test_login_valid_credentials() in pytest |
One scenario typically explodes into 5–15 test cases. "Verify user can log in" becomes: valid credentials, wrong password, unregistered email, empty fields, locked account, expired password, case-insensitive email, trailing whitespace, SQL-injection string, account with 2FA enabled…
That explosion is the actual work of test design.
2. The test case template¶
Use this structure. Every column earns its place.
| Field | Purpose | Example |
|---|---|---|
| Test Case ID | Unique, stable reference for reports and defects | TC-LOGIN-004 |
| Module / Feature | Groups cases, enables filtered runs | Authentication |
| Title | One line, states the condition being tested | Login fails with unregistered email |
| Priority | Execution order under time pressure | High |
| Type | Functional / Negative / Boundary / UI / Security | Negative |
| Preconditions | State that must exist before step 1 | App is deployed; no account exists for qa.unknown@test.com |
| Test Data | Exact values used | email: qa.unknown@test.com, password: Valid#123 |
| Steps | Numbered, one action each | See below |
| Expected Result | Observable, specific, verifiable | Error "No account found for this email" appears below the email field; user remains on /login |
| Actual Result | Filled at execution time | As expected |
| Status | Pass / Fail / Blocked / Skipped / Not Run | Pass |
| Requirement ID | Link back for traceability | REQ-AUTH-02 |
| Executed By / Date | Audit trail | B. Kumar, 2026-07-30 |
A fully worked example¶
TC-LOGIN-004 — Login fails with unregistered email
| Module | Authentication |
| Priority | High |
| Type | Negative |
| Requirement | REQ-AUTH-02 |
| Preconditions | Application reachable at staging URL; no account exists for qa.unknown@test.com; browser cache cleared |
| Test Data | Email qa.unknown@test.com, Password Valid#123 |
| Step | Action | Expected Result |
|---|---|---|
| 1 | Navigate to https://staging.example.com/login |
Login page loads within 3 s; Email, Password fields and "Sign in" button are visible |
| 2 | Enter qa.unknown@test.com in the Email field |
Value appears; no inline error yet |
| 3 | Enter Valid#123 in the Password field |
Value appears masked as dots |
| 4 | Click Sign in | Button shows loading state, then re-enables |
| 5 | Observe the page | Error message "No account found for this email" appears below the Email field; URL is still /login; password field is cleared; no session cookie is set |
Note what step 5 does: it states multiple observable conditions, including a negative one (no session cookie). A vague expected result like "login fails" is the single most common defect in test case writing — it lets two testers reach opposite conclusions from the same screen.
3. Rules for writing steps¶
- One action per step. "Enter email and password and click Sign in" is three steps. When it fails, you need to know which action failed.
- Use exact values, not descriptions. "Enter a long name" → "Enter
Aaaaa…(256 characters)". Reproducibility depends on it. - Name UI elements as the user sees them. "Click the Sign in button", not "click the submit control".
- Expected results must be observable. "System processes the request" is
not observable. "Order confirmation number appears in the format
ORD-#####" is. - Independence. A case should not depend on another case having run first. If it truly does, say so in Preconditions and note the dependency.
- Include cleanup if the case leaves state behind. A case that creates a user should say how to remove it, or the second run will fail for the wrong reason.
- No implementation detail in a manual case. Referring to a database table or CSS class ties the case to a version of the system it will outlive.
The stranger test
Before you call a test case done, ask: could someone who has never seen this application execute it exactly as written and reach an unambiguous pass/fail? If not, it isn't finished.
4. Positive and negative cases¶
Every functional requirement needs both, and juniors systematically under-write the negative side.
- Positive (happy path): the system does what it should with valid input.
- Negative: the system refuses correctly with invalid input, and refuses gracefully — a clear message, no crash, no data written, no stack trace leaked.
For a "Register with email and password" requirement:
| ID | Type | Title |
|---|---|---|
| TC-REG-001 | Positive | Register successfully with valid unique email and strong password |
| TC-REG-002 | Negative | Registration rejected when email format is invalid (user@, user@@x.com, @x.com) |
| TC-REG-003 | Negative | Registration rejected when email already registered |
| TC-REG-004 | Negative | Registration rejected when password below minimum length |
| TC-REG-005 | Negative | Registration rejected when required field left empty |
| TC-REG-006 | Boundary | Registration succeeds at exactly the minimum password length |
| TC-REG-007 | Boundary | Registration succeeds at exactly the maximum email length |
| TC-REG-008 | Security | Script tag in the name field is stored/rendered escaped, not executed |
| TC-REG-009 | UI | Password strength indicator updates as the user types |
| TC-REG-010 | Functional | Confirmation email is received within 2 minutes at the registered address |
Ten cases from one sentence of requirement. That ratio is normal.
5. Deriving cases from a requirement¶
Take a real, slightly under-specified requirement:
REQ-CART-07: A customer can apply one promotional code per order. Codes are case-insensitive. An expired or invalid code shows an error. Codes cannot be applied to orders under ₹500.
Work through it clause by clause. Each clause is a source of cases, and each gap is a question for the business analyst.
| Clause | Cases it generates | Question it raises |
|---|---|---|
| "one promotional code per order" | Apply one code (pass); apply a second code (behaviour?) | Does the second code replace the first, or is it rejected? |
| "case-insensitive" | SAVE20, save20, SaVe20 all apply |
Is leading/trailing whitespace trimmed? |
| "expired … shows an error" | Apply a code expired yesterday | What's the exact message? Is a code expiring today valid? |
| "invalid … shows an error" | Apply a nonsense code; apply an empty code | Same message for expired and invalid, or different? |
| "orders under ₹500" | ₹499 (reject), ₹500 (?), ₹501 (accept) | Is ₹500 exactly "under"? Is the threshold pre- or post-tax? |
| Unstated | Apply code, then remove an item so total drops below ₹500 | Does the discount get revoked? This is the interesting one. |
That last row is where testers earn their keep. The requirement never mentions it, the developer probably didn't implement it, and it's a real bug waiting in production. Reading requirements for what they omit is a core skill.
Log ambiguities as questions rather than guessing — an assumption baked into a test case becomes a false failure or, worse, a missed defect.
6. Priority and severity in test design¶
At design time you assign priority to cases (severity comes later, per defect — see Module 4).
| Priority | Meaning | Typical cases |
|---|---|---|
| P1 / Critical | Must run on every build; blocks release if failing | Login, checkout, payment, data integrity |
| P2 / High | Core features; run every regression cycle | Search, profile update, order history |
| P3 / Medium | Secondary flows; run per release | Preferences, filters, sorting |
| P4 / Low | Cosmetic or rare | Tooltip wording, footer links |
Priority is what you use when the release is tomorrow and you have time for 40 of your 200 cases. If you have never had to make that call, write your priorities as if you will — because you will.
7. Requirements Traceability Matrix (RTM)¶
An RTM maps requirements to the test cases that verify them, in both directions. Its purpose is to answer two questions that management will absolutely ask:
- Forward: "Is every requirement covered by at least one test?"
- Backward: "Does every test trace to a real requirement?" (Tests that don't are either scope creep or a sign of an undocumented requirement.)
Example RTM¶
| Req ID | Requirement summary | Test Case IDs | Cases | Status | Defects |
|---|---|---|---|---|---|
| REQ-AUTH-01 | User logs in with valid credentials | TC-LOGIN-001, TC-LOGIN-002 | 2 | Pass | — |
| REQ-AUTH-02 | Invalid credentials are rejected with a message | TC-LOGIN-003, TC-LOGIN-004, TC-LOGIN-005 | 3 | 2 Pass, 1 Fail | BUG-118 |
| REQ-AUTH-03 | Account locks after 5 failed attempts | TC-LOGIN-006, TC-LOGIN-007 | 2 | Not Run | — |
| REQ-CART-07 | One promo code per order, min ₹500 | TC-CART-020 … TC-CART-027 | 8 | 6 Pass, 2 Blocked | BUG-121 |
| REQ-CART-08 | Cart persists across sessions | none | 0 | Gap | — |
The value of the RTM is that last row. REQ-CART-08 has zero coverage, and
the matrix makes that impossible to miss in a status meeting. A coverage gap you
can see is a decision; a coverage gap you can't see is an incident.
Coverage percentage here: 4 of 5 requirements covered = 80% requirement coverage. Note this is requirement coverage, not code coverage — different metric, different meaning (Module 3).
Keep the RTM cheap
An RTM maintained by hand across 800 cases will rot. Most test management tools (TestRail, Zephyr, Xray, qTest, or even a well-structured spreadsheet) generate it from the links you already record in the Requirement ID field. Record the link once, at case-writing time.
8. Where test cases live¶
| Option | Good for | Watch out for |
|---|---|---|
| Spreadsheet (Excel / Google Sheets) | Small teams, fast start, zero cost | No execution history, merge conflicts, no defect linking |
| Test management tool (TestRail, Zephyr, Xray, qTest) | Teams, audit trails, RTM generation, run history | Licence cost; needs discipline to keep tidy |
| In the repo as Markdown/YAML | Version control, review via pull request, lives with the code | Reporting is manual unless you build it |
| Automated tests as documentation | Always current — the test is the spec | Only covers what's automated; unreadable to non-technical stakeholders |
Whichever you pick, the fields in section 2 are what you need. The tool is an implementation detail; the discipline is not.
How It Actually Works¶
Equivalence partitioning and boundary value analysis aren't just heuristics testers use in their heads — they're the same reasoning that automated fuzzers and property-based test generators (you'll meet Hypothesis in Level 3) implement mechanically.
An equivalence class is, formally, a partition of the input domain such that the
program's control-flow graph treats every member identically — every value in the
class drives execution down the same sequence of branches. A boundary value sits at
the edge of a partition specifically because comparison operators (<, <=, >,
>=) are where off-by-one defects live: the CPython bytecode for if age >= 18:
compiles to a COMPARE_OP instruction, and a boundary test (age = 17, age = 18)
is deliberately chosen to flip which branch that single instruction's result sends
execution down. This is exactly why boundary tests catch far more real bugs per test
written than an equal number of "random" values from the middle of a partition — you
are directly targeting the comparison instructions where a < was typo'd for <=.
When you later write a Requirements Traceability Matrix, you're manually building
what a coverage tool builds automatically from sys.settrace data: a mapping from
"thing that must be true" to "code path that proves it." The RTM is the human-side
ledger; line/branch coverage is the machine-side ledger for the same underlying
question — does every requirement have an execution path that verifies it?
Exercise¶
Using the search feature of any e-commerce site you can access:
- Write one test scenario and derive eight test cases from it, using the full template from section 2 (all fields, numbered steps, specific expected results). Include at least: two positive, four negative, and two boundary cases.
- For each case, assign a priority (P1–P4) and justify the two P1s in one sentence each.
- Take this requirement and list every ambiguity you'd raise with the business analyst before writing cases:
"Search results are sorted by relevance. Users can filter by price range and rating. Searching for a term with no matches shows a suggestion."
- Build an RTM for the four requirements below against the cases you wrote,
and identify any coverage gaps:
REQ-SRCH-01keyword search returns matching products ·REQ-SRCH-02results sortable by price ·REQ-SRCH-03no-match shows suggestions ·REQ-SRCH-04search history saved for logged-in users.