The anatomy of a good test case
Every quality is a visible edit, not an adjective
This whole guide is one bad test case becoming a good one. Watch a vague, compound, order-dependent case turn atomic, precise, independent, and traceable, one edit at a time.
How to write a good test case, shown as one rewrite
A good test case is an artifact, not a paragraph of hope. This guide rebuilds one genuinely bad case into a clean one, edit by edit, then derives the whole positive, negative, and edge suite from a single real field.
A test case is an artifact, not a paragraph of hope#
Most guides on how to write a good test case list adjectives and stop. They tell you a case should be clear, then leave "clear" undefined. However, a test case is a small software engineering artifact. It has fields, and each field does a job. Because the fields are concrete, you can inspect them, review them, and version them like code.
Think of a test case as a tiny contract. It states the world before the test, the exact actions, and the single result that decides pass or fail. Therefore two people reading the same case reach the same verdict. That property is the whole point. In short, a good case removes judgment from execution and puts it into authoring, where it belongs.
Where the artifact ends and strategy begins#
This post owns the craft of the test-case artifact. It covers the anatomy, the reliability qualities, and how to generate a case set. It does not cover which areas deserve the most testing budget. That allocation question is a separate discipline. For that layer, read the discipline behind high-quality delivery and software testing techniques. Because the two topics are distinct, this guide stays on the artifact and links out for strategy.
The anatomy: seven fields, each earning its place#
Start with the shape. A reliable test case fills seven fields, and each one prevents a specific failure. The specimen below labels all seven on a real case, TC-503, from the free-shipping feature we return to later. Read each callout as the reason that field exists.
- Stable IDA permanent handle (TC-503) so a result, a bug report, and a requirement can all point at the same case without ambiguity.
- Descriptive titleNames the one behavior under test, so a reader knows what fails before reading a single step.
- PreconditionsThe exact starting state. Removes the hidden setup that makes a case pass on one machine and fail on another.
- Precise ordered stepsNumbered actions with no room for interpretation, so any tester reproduces the same run.
- Test dataThe concrete input values. A case with vague data ("some amount") cannot be repeated or automated.
- Single expected resultOne checkable outcome. This is the hinge of the whole case, so it gets its own section below.
- Post-conditionsThe state the test leaves behind, so the next case starts clean and nothing leaks between runs.
Why exactly ONE expected result per case#
The single-expected-result rule is the hinge of the whole artifact. A case with two expected results is really two cases wearing one ID. Because a run then passes half and fails half, the result is ambiguous. Which half failed? You cannot tell from a green or red mark.
One expected result keeps the verdict binary. Therefore a failure points at exactly one behavior, and the case stays atomic. This rule feeds directly into the rewrite that follows. In practice, when a case tempts you to add "and also check X", that X is your next case.
How to write a good test case: one bad case, rebuilt#
Here is the spine of the guide. Below is one genuinely bad test case. It is vague, compound, order-dependent, untraceable, and brittle. It fails all five reliability qualities at once. Step through the five edits with Back and Next. Each edit restores exactly one quality, highlights the field it changes, and flips one scorecard cell from Fail to Pass.
Start: the bad case, before any edit
Scorecard: 0 of 5 qualities restored
- Atomicone behavior, one pass or failFail
- Unambiguousany tester, same verdictFail
- Independentno leftover state from another caseFail
- Traceablelinked to an acceptance criterionFail
- Maintainableasserts on behavior, not brittle locatorsFail
The bad case (unchanged)
- ID
- (none)
- Title
- Test the cart
- Preconditions
- Continue from TC-4 (cart already has items)
- Test data
- (unspecified)
- Steps
- Add some products, change the amount, go to checkout, click the green button in the top-right, then verify shipping works
- Expected result
- Shipping is correct and it works
- Post-conditions
- (unspecified)
- Traces to
- (none)
Rebuilt so far
- ID
- (none)
- Title
- Test the cart
- Preconditions
- Continue from TC-4 (cart already has items)
- Test data
- (unspecified)
- Steps
- Add some products, change the amount, go to checkout, click the green button in the top-right, then verify shipping works
- Expected result
- Shipping is correct and it works
- Post-conditions
- (unspecified)
- Traces to
- (none)
All five edits, and the final clean case
- Edit 1 restores Atomic. Split the compound case down to one behavior: the $50.00 free-shipping threshold.
- Edit 2 restores Unambiguous. Replaced "it works" with a precise expected result and named the test data.
- Edit 3 restores Independent. Dropped "continue from TC-4" for a self-contained fresh-cart precondition.
- Edit 4 restores Traceable. Gave it a stable ID and linked it to acceptance criterion AC-2.
- Edit 5 restores Maintainable. Asserted on the shipping value via a stable identifier, and pinned the post-condition.
Final case (TC-503): a single behavior, a fresh precondition, one precise expected result, a post-condition, and a link to AC-2. It passes all five qualities.
Atomic: one behavior, one pass or fail#
The bad case tests four things in one breath. It adds products, changes an amount, checks out, and verifies shipping. Because it bundles behaviors, a red mark tells you nothing about which one broke. The first edit narrows the case to a single behavior: does free shipping apply at the $50.00 threshold. Now one failure means one thing.
Unambiguous: any tester, same verdict#
The original expected result reads "shipping is correct and it works". However, "works" is not checkable. Two testers can disagree on whether a run passed. The second edit replaces that phrase with a precise outcome: shipping fee equals $0.00 and order total equals $50.00. It also names the test data. Consequently the verdict no longer depends on who is looking.
Independent: no leftover state from another case#
The bad case begins with "continue from TC-4". That single phrase is the order-dependence trap. Because the case silently relies on another case's leftover cart, running it alone fails, and running the suite out of order fails too. The third edit deletes that dependency and adds a self-contained precondition: a fresh, empty guest cart. Now the case sets up its own world.
Traceable: linked to an acceptance criterion#
The original has no ID and no link to a requirement. So when the shipping rule changes, nobody knows which cases to update. The fourth edit gives the case a stable ID, TC-503, and a link to acceptance criterion AC-2. Because the link exists, impact analysis becomes a lookup instead of a hunt. We show that mapping as a real matrix later.
Maintainable: assert on behavior, not brittle locators#
The bad case says "click the green button in the top-right". That instruction breaks the moment a designer moves or recolors the button. Maintainability is not a self-healing tool you buy. Instead it is an authoring discipline. The fifth edit asserts on behavior through a stable identifier and checks the shipping value, not the pixel position. Therefore a harmless redesign no longer turns the case red.
Good test cases are also documentation#
Once a case is precise, it stops being only a check. It becomes the executable specification of a behavior. A new teammate can read TC-503 and learn exactly how free shipping should behave, without asking anyone. Because the case names the input, the action, and the one expected result, it doubles as a worked example. This is an under-used payoff of learning how to write a good test case.
Generating the case set: partitions and boundaries#
Now flip from rewriting to generating. You rarely need many cases. Instead you need the right few. Two classic black-box techniques derive them from a single field, and neither requires guessing. Consider one real requirement, stated as an acceptance criterion.
From that one field, the subtotal, both techniques fall out. First equivalence partitioning splits the input space into classes. Then boundary value analysis probes the edges between classes. Together they produce a small, complete suite. Let us derive it.
Equivalence partitions: one representative per class#
Equivalence partitioning groups inputs that the system should treat the same way. Per ISO/IEC/IEEE 29119-4, one representative value covers its whole class. The subtotal has three partitions. First an invalid class, subtotal below $0.00, which is impossible and gets rejected. Second a valid below-threshold class, $0.00 to $49.99, which is charged $6.99. Third a valid at-or-above class, $50.00 and up, which ships free. Therefore two representatives, say $10.00 and $120.00, cover the two valid classes.
Boundary values: where the defects actually hide#
Partitions tell you the classes. However, defects cluster at the edges between them, not in the middle. Boundary value analysis targets those edges. At the $50.00 boundary, three values matter: $49.99, $50.00, and $50.01. Here is why the boundary bites. A developer often codes the check as subtotal greater-than 50 instead of greater-than-or-equal to 50. Because of that one character, the code charges $6.99 at exactly $50.00, which violates the requirement. Only a case sitting on $50.00 catches it.
Derive the suite from the field#
The forge below turns the technique into an engine. Set the threshold and watch the three partitions, the boundary triple, and the atomic five-case suite regenerate. Then flip the code condition from the correct greater-than-or-equal to the off-by-one greater-than. Because the exact-boundary case sits on the threshold, it is the one row that flips to a defect. That is the boundary case earning its keep.
Condition subtotal >= threshold matches the requirement. All five cases pass. Flip to the off-by-one condition to see which case catches the defect.
Step 1: three equivalence partitions
- Invalidsubtotal < $0.00Impossible input: reject, do not add a positive case
- Valid, below threshold$0.00 to $49.99Representative $10.00 to shipping $6.99
- Valid, at or above threshold$50.00 and upRepresentative $120.00 to shipping $0.00
Step 2: the boundary triple, where defects hide
- $49.99just belowexpected shipping $6.99
- $50.00exact boundaryexpected shipping $0.00
- $50.01just aboveexpected shipping $0.00
Step 3: the derived five-case atomic suite
| ID | Subtotal | Expected shipping | Expected total | Type | Result under this condition |
|---|---|---|---|---|---|
| TC-501 | $10.00 | $6.99 | $16.99 | positive, deep partition | Pass |
| TC-502 | $49.99 | $6.99 | $56.98 | edge, just below | Pass |
| TC-503 | $50.00 | $0.00 | $50.00 | edge, exact boundary | Pass |
| TC-504 | $50.01 | $0.00 | $50.01 | edge, just above | Pass |
| TC-505 | $120.00 | $0.00 | $120.00 | positive, deep partition | Pass |
Positive, negative, edge: a byproduct, not a checklist#
Notice what just happened. The famous case "types" fell out of the derivation for free. A positive case is a representative deep inside a valid partition, like $120.00. A negative case rejects an invalid partition, like a subtotal below $0.00. An edge case sits on a boundary, like $49.99 or $50.00. Therefore you never enumerate types from memory. Instead the partitions and boundaries hand them to you, and the set stays complete by construction. In short, this is the generate-not-enumerate half of how to write a good test case.
Traceability, shown as a real mapping#
Traceability is usually asserted and never shown. So here is the actual mapping. Each clause of the acceptance criterion links to the concrete case IDs that cover it. Read it as a lookup: change a clause, and the matrix names the exact cases to revisit.
| AC clause | Partition | Representative subtotals | Case IDs | What it proves |
|---|---|---|---|---|
| AC-1: an invalid subtotal is rejected | PartitionInvalid (below $0.00) | Representative subtotalsnone (rejected input) | Case IDsno positive case | What it provesInput validation, handled before the shipping rule |
| AC-2: free shipping at subtotal >= $50.00 | PartitionAt or above threshold | Representative subtotals$50.00, $50.01, $120.00 | Case IDsTC-503, TC-504, TC-505 | What it provesThe free branch, including the exact boundary |
| AC-3: flat $6.99 below $50.00 | PartitionBelow threshold | Representative subtotals$10.00, $49.99 | Case IDsTC-501, TC-502 | What it provesThe paid branch, including the just-below edge |
When NOT to write a scripted case#
A scripted case is a tool, not a religion. Sometimes it is the wrong tool. Writing one has a real cost: authoring, maintenance, and the false comfort of a green check. So route each behavior before you script it. The decision flow below asks three questions in order, and only one path ends in a scripted case.
Route the rest to exploratory and automation#
The routing decision hands two paths to sibling posts. When a behavior is still fuzzy, send it to exploratory testing, which surfaces the edge cases scripted cases miss. Then, once a case is stable and worth repeating, automate it. For that step, read our guide to choosing between Selenium, Cypress, and Playwright for automating those test cases. Because each tool fits a different job, the routing decision tells you which behaviors to hand over. Manual work still sharpens the automated suite, as shown in how manual testing sharpens automation quality.
A clean test-case template you can adopt today#
Here is the payoff you can copy. The template below carries all seven fields in one structured block. Fill each field, keep exactly one expected result, and link the case to an acceptance criterion. Because the shape is fixed, every case you write stays reviewable, repeatable, and traceable.
# Test case template (copy, then fill each field)
id: TC-503
title: Free shipping applies when subtotal reaches the $50.00 threshold
preconditions:
- Guest checkout, fresh empty cart
test_data:
subtotal: $50.00
steps:
- Add items totaling exactly $50.00
- Select the checkout action ([data-testid=checkout])
expected_result: Shipping fee = $0.00 and order total = $50.00
post_conditions:
- Cart preserved; no order placed
traces_to: AC-2 (subtotal >= $50.00 grants free standard shipping) Adopt this shape and the reliability qualities come almost for free. First one behavior per case keeps it atomic. Then a precise expected result keeps it unambiguous. Next a self-contained precondition keeps it independent. Finally the traces field keeps it traceable, and asserting on behavior keeps it maintainable. That is the entire discipline of how to write a good test case, in one reusable block.