Writing test cases and test plans
Turn requirements into clear, repeatable checks.
Topic 5 — Writing test cases and test plans
Goal: Turn requirements into clear, repeatable checks.
Lesson 5.1 — A check anyone could run
In her second week at FreshCart, Nadia is handed a sticky note: "test the promo code box." She tests it. It works. She moves on. The next morning Marcus, her mentor, asks what exactly she tried: which codes, in what order, what she expected to happen. Nadia can't fully reconstruct it. She tested something, but nobody, including her, could repeat it and be sure of the same result.
That gap is the reason test cases exist. A test case is a single, specific check written so clearly that anyone could follow it and get the same result. It is the basic unit of organized testing: the difference between "I poked at it" and "this exact behavior was verified."
Marcus shows her the four parts every solid test case has:
- Title — what it checks, in plain words. "Apply valid promo code SAVE10 to a $20 cart."
- Preconditions — what must already be true before you start. "A registered user is logged in; SAVE10 is active; cart subtotal is $20.00."
- Steps — the exact actions, numbered. 1. Open the cart. 2. Type SAVE10 in the promo field. 3. Tap Apply.
- Expected result — what should happen. "A 10% discount appears; cart total becomes $18.00; a green 'Code applied' message shows."
Nadia recognizes the shape instantly. For eight years as a pharmacy technician she'd worked from labels that spelled out drug, dose, and what the filled bottle should look like, so the next shift could check her work and reach the same answer. A test case is that discipline pointed at software.
If a teammate can't run your test case and land on the same result, you've written down a memory, not a test case.
Lesson 5.2 — Define "pass" before you press anything
Here's the move that separates a tester from someone clicking around: writing the expected result down before you run the test.
Running a test means comparing the actual result (what the app really did) to the expected result (what the requirement says it should do). If the two match, the test passes. If they differ, you have found a bug. That comparison is testing — everything else is setup.
Why commit the expected result to writing first? Because it keeps you honest. Nadia applies SAVE10 and the total drops to $18.50. If she hadn't decided beforehand that the right answer was $18.00, it's dangerously easy to glance at "$18.50, discount applied" and call it a pass, since the discount did something. With the expected $18.00 written down, the half-dollar gap is obvious, and she's found a real bug: the code is taking 7.5% off, not 10%.
Marcus has a line for this.
You can't grade your own homework if you write the answer key after you peek. Define pass first.
Priya, the developer who built checkout, is the one who gets Nadia's bug reports — and she appreciates this rigor rather than resenting it. "Expected $18.00, actual $18.50" tells Priya exactly what's wrong in one line. A vague "the promo seems off" would cost both of them an afternoon.
Lesson 5.3 — Where test cases come from: the spec
Nadia's instinct her first week was to invent tests from her own head. That works until you miss the thing nobody told you to check.
Test cases come from the requirements — the spec. At FreshCart, those are written by Tom, the product manager: short statements of what the product is supposed to do. "Promo codes apply a percentage discount to the cart subtotal before tax. Expired or invalid codes are rejected with an error. One code per order." For each thing the spec says the product should do, Nadia writes test cases that verify it does that.
The aim is coverage — testing enough of the important scenarios that she can stand up in a release meeting and honestly say "this works." Coverage is measured by hitting the scenarios that matter, not by the sheer number of tests. Tom's three sentences above already imply several distinct checks: a valid code discounts correctly, an expired code is refused, a second code is blocked.
Working from the spec also gives Nadia ground to stand on. When she files a bug and a developer says "that's not really wrong," she points at the requirement: the spec says discount before tax; the app applied it after. The argument stops being her opinion versus theirs and becomes the build versus the agreed-upon spec. That's why Marcus tells her to always test against something written, never against a vibe.
Lesson 5.4 — Three families: happy path, edges, errors
For any single feature, a beginner tests the obvious case and stops. A good tester covers three families on purpose.
The happy path is the feature working with valid, expected input, also called positive testing. Valid code, normal cart, discount applies. This is the case the developer definitely tried, so it's the least likely to reveal anything. It's necessary, but it's the floor, not the ceiling.
Edge cases live at the limits, where well-behaved code often cracks: empty fields, very long input, special characters and emojis, the maximum and minimum allowed values, zero, and negatives. What happens if Nadia pastes a 200-character string into the promo box? Applies a code to an empty cart? Types SAVE10🍅 with an emoji on the end? Enters the exact maximum order amount the discount allows? These are the inputs the developer was too busy to imagine.
Error cases are things going wrong on purpose — negative testing: invalid input, a wrong or expired code, no internet. A good error case checks two things: the app refuses the bad input and it fails gracefully, with a clear message instead of a crash or a silent nothing. Nadia turns off wifi mid-checkout and watches: does FreshCart say "you're offline, we'll retry," or spin forever and lose her cart?
Edges and errors are where the bugs hide, because that's exactly where attention ran out during the build. Marcus puts it bluntly: anyone can confirm the happy path; you get hired for the other two.
Lesson 5.5 — The test plan: strategy, and risk-based priorities
A test case is one check. A test plan is the bigger-picture strategy for a whole feature or release. Before FreshCart ships the redesigned checkout, Marcus and Nadia write one, and it answers:
- What is in scope — and, just as importantly, what is explicitly out. "We're testing promo codes, payment, and order total. We are not re-testing the live map tracking this release." Naming what you're skipping is a feature of a plan, not a gap in it.
- How it's tested — which test types from Topic 4: manual exploratory passes, a regression run, some API checks on the discount service.
- Which environments — devices, browsers, OS versions. FreshCart's users skew mobile, so iPhone Safari and Android Chrome come first; an old desktop browser can wait.
- Who and when — Nadia owns manual cases, Dev runs the Playwright regression suite, and it all happens before the Thursday release cutoff.
- What "done" means — the exit bar. "Ready to release" might mean: all high-priority cases pass, no open critical or high-severity bugs, payment verified on both target phones.
The reason a plan must declare what it won't cover is a hard truth from the ISTQB body of knowledge, the field's standard reference: exhaustive testing is impossible. The number of possible inputs and paths through even a simple feature is effectively infinite — a single text field accepting twenty characters has more possible values than you could ever enumerate. You can never test everything. A plan that pretends otherwise is lying; a plan that names its boundaries is doing its actual job.
Which leads to the only sane question: of the infinite things she could check, which deserve Nadia's limited hours? That question makes testing risk-based prioritization. She spends effort where the stakes are highest — the most important features, the most likely failure points, the areas where a bug would hurt most. At FreshCart, payment and order total are sacred; a wrong charge means a furious customer and a refund. The "remember my last tip percentage" setting is nice, but if it glitches, nobody's morning is ruined. So when the clock is tight, Nadia pours her hours into checkout and promo math, and only lightly pokes the tip setting. Tom helps her rank by business urgency when she's unsure which area matters more.
The classic rookie mistake is the inverse: an hour spent meticulously testing a rarely-used toggle while checkout goes barely touched, then a payment bug reaches real customers. A thoughtful plan exists precisely to stop that — it aims the limited time at the things that would actually cost FreshCart money and trust.
Worked example — "Set delivery age check" field
FreshCart adds an age field for orders that include alcohol: the customer must enter an age, and 18 to 65 inclusive is the valid range. Tom's spec is one line. Watch Nadia turn it into a small, deliberate set of test cases instead of clicking randomly.
She writes one reusable shell — Preconditions: logged-in user, alcohol item in cart, on the age-entry screen — then generates inputs systematically across the three families:
| Input | Family | Expected result |
|---|---|---|
| 30 (a normal valid age) | happy path | Accepted; checkout proceeds |
| 18 (minimum boundary) | edge | Accepted |
| 65 (maximum boundary) | edge | Accepted |
| 17 (just below minimum) | edge | Rejected with "must be 18+" |
| 66 (just above maximum) | edge | Rejected with a clear message |
| 0 | edge | Rejected, no crash |
| -5 (negative) | edge/error | Rejected, no crash |
abc (letters) | error | Rejected; field refuses non-numbers |
| (blank) | error | Rejected; "this field is required" |
Nine cases, not nine hundred, and not one random click. Each row exists for a reason: the normal value, both boundaries, just-outside both boundaries, zero, a negative, letters, and blank. That deliberate scenario-generation, working outward from one requirement to the inputs most likely to break it, is the real craft of QA. When Nadia runs it and 66 is quietly accepted, she has a precise, repeatable bug to hand Priya: "Expected reject for 66, actual accepted. Steps attached."
Key terms
- Test case — one specific, repeatable check: title, preconditions, numbered steps, expected result.
- Expected result — what should happen, written down before you run the test.
- Actual result — what the app really did; comparing it to the expected result is testing.
- Coverage — testing enough important scenarios to be confident a feature works.
- Happy path / positive testing — the feature working with valid, expected input.
- Edge case — input at the limits: empty, very long, special chars/emoji, max/min, zero, negatives.
- Error case / negative testing — invalid input, wrong values, no internet; checks it fails gracefully.
- Test plan — the strategy: scope (and out-of-scope), test types, environments, who/when, "done."
- Risk-based prioritization — spending limited testing effort on the highest-risk, highest-stakes areas.
Try this
Pick one everyday field you can see right now — a website's email signup box, a search bar, a quantity selector. Find or guess its one requirement (e.g., "email must contain an @ and a domain"). On paper, write a small set of test cases that covers all three families: one happy path, three or four edge cases (blank, very long, special characters, an emoji), and two error cases (clearly invalid input, and what happens with no connection). For each, write the expected result before you try it. Then run them. Notice how the cases you'd never have clicked at random are the ones that surface odd behavior — that's the muscle this topic builds.
Common pitfalls
- Writing the expected result after you look. Peeking first lets you rationalize whatever happened into a "pass." Decide the right answer before you press anything.
- Testing only the happy path. Confirming valid input works is the floor. The bugs cluster in the edges and error cases the developer didn't have time to imagine.
- Random clicking instead of systematic cases. Poking around feels like testing but isn't repeatable and can't prove coverage. Generate inputs deliberately: boundaries, just-outside, zero, negative, blank.
- Trying to test everything. Exhaustive testing is impossible, so chasing it means you run out of time before reaching checkout. Prioritize by risk; let the plan name what you'll skip.
Key takeaways
- A test case is a single repeatable check: title, preconditions, numbered steps, expected result. Testing = comparing actual to expected; a mismatch is a bug.
- Write the expected result first — it keeps you honest and gives developers a precise report.
- Test cases come from the requirements/spec; aim for coverage of three families: happy path, edge cases, error cases.
- Generate inputs systematically (boundaries, just-outside, zero, negative, blank, letters) — not by random clicking.
- A test plan is the strategy (scope and out-of-scope, types, environments, who/when, "done"). Since exhaustive testing is impossible, testing is risk-based prioritization — protect checkout, not the rarely-used toggle.
Preparing your quiz…