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.

  1. Stable IDA permanent handle (TC-503) so a result, a bug report, and a requirement can all point at the same case without ambiguity.
  2. Descriptive titleNames the one behavior under test, so a reader knows what fails before reading a single step.
  3. PreconditionsThe exact starting state. Removes the hidden setup that makes a case pass on one machine and fail on another.
  4. Precise ordered stepsNumbered actions with no room for interpretation, so any tester reproduces the same run.
  5. Test dataThe concrete input values. A case with vague data ("some amount") cannot be repeated or automated.
  6. Single expected resultOne checkable outcome. This is the hinge of the whole case, so it gets its own section below.
  7. Post-conditionsThe state the test leaves behind, so the next case starts clean and nothing leaks between runs.
The seven fields of a reliable test case, labelled on TC-503. Values are illustrative.

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.

Rewrite workbench: rebuild one bad test case, one quality at a time

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
  1. Edit 1 restores Atomic. Split the compound case down to one behavior: the $50.00 free-shipping threshold.
  2. Edit 2 restores Unambiguous. Replaced "it works" with a precise expected result and named the test data.
  3. Edit 3 restores Independent. Dropped "continue from TC-4" for a self-contained fresh-cart precondition.
  4. Edit 4 restores Traceable. Gave it a stable ID and linked it to acceptance criterion AC-2.
  5. 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.

Step through five edits with Back and Next. Each edit is tagged with the quality it restores; the changed field highlights and the scorecard cell flips. With JavaScript off, the bad case, the all-Fail scorecard, and a full list of every edit still render. Values are illustrative.

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.

Boundary case forge: derive the partitions, boundary, and suite from one field
Code condition under test

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

Minimal reliable suite: two partition representatives plus three boundary values (illustrative)
IDSubtotalExpected shippingExpected totalTypeResult under this condition
TC-501$10.00$6.99$16.99positive, deep partitionPass
TC-502$49.99$6.99$56.98edge, just belowPass
TC-503$50.00$0.00$50.00edge, exact boundaryPass
TC-504$50.01$0.00$50.01edge, just abovePass
TC-505$120.00$0.00$120.00positive, deep partitionPass
Set the free-shipping threshold and read the derived partitions, boundary triple, and five-case suite. The segmented toggle switches the code condition; the off-by-one condition flips exactly the boundary case (TC-503) to a defect. With JavaScript off, the default $50.00 derivation and full table still render. Values are illustrative.

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.

Acceptance-criterion clauses mapped to the concrete case IDs that cover them (illustrative)
AC clausePartitionRepresentative subtotalsCase IDsWhat it proves
AC-1: an invalid subtotal is rejectedInvalid (below $0.00)none (rejected input)no positive caseInput validation, handled before the shipping rule
AC-2: free shipping at subtotal >= $50.00At or above threshold$50.00, $50.01, $120.00TC-503, TC-504, TC-505The free branch, including the exact boundary
AC-3: flat $6.99 below $50.00Below threshold$10.00, $49.99TC-501, TC-502The 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.

A routing decision: does this behavior deserve a scripted case?Route each behavior before scripting it. A stable, repeatable, machine-checkable behavior earns a scripted case. An unstable or human-judged behavior goes to exploratory testing; a one-off check needs no script at all.

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.yaml · yaml
# 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.

Talk to us about test design and QA

Keep reading