Skip to content
Independent guides for QA & test automationRSSEditorial policy
QA Vibes

How to Write Effective Test Cases: Worked Examples on a Real Checkout Form

A practical guide to test cases: what to include, three techniques for choosing inputs, ten real cases for a checkout form with the results we observed, and how the cases become an automated test. Three of the ten raised questions the form's requirements don't answer.

QA Vibes EditorialPublished Updated 8 minTested with Playwright 1.63.0, Chromium 153Revision history ↓

Key takeaways

  • Choose inputs with equivalence partitions, boundary values, and error guessing.
  • Write the expected result before you run the case.
  • On a three-field checkout form, ten cases raised three questions the requirements didn't answer.
  • Cases with the same steps and different data become one data-driven automated test.
Contents (10 sections)

Introduction

A test case is a written check: the conditions, the steps, the input, and the result you expect. Good test cases do two jobs. They let anyone repeat a check the same way, and, if the inputs are chosen well, they find bugs that random clicking misses.

This guide works through one small, real example: the "Your Information" step of the checkout on Sauce Demo, a public practice shop. The form has three fields (First Name, Last Name, and Zip/Postal Code) and a Continue button. We designed the cases, ran every one on the standard_user account in September 2026, and recorded what happened.

What a test case contains

Field Purpose Example
ID Refer to the case in bugs and reports CHK-INFO-03
Title Say what's checked, in one line Checkout requires a last name
Preconditions The state before step 1 Logged in as standard_user, one item in the cart, on "Checkout: Your Information"
Steps Numbered actions, one per step 1. Enter "Ada" in First Name…
Test data The exact values used First Name "Ada", Last Name empty, Zip "10115"
Expected result What must happen, precisely Error "Error: Last Name is required"; the page doesn't change
Priority How much it matters High
Traceability The requirement or story it checks Story SHOP-142, if your team uses them

Two fields decide whether a case is useful:

  • Preconditions keep the steps short, and make the case repeatable by someone who wasn't there when it was written.
  • The expected result must be something you can check, not a feeling. "Form works" can't fail. "Error: Last Name is required" can.

Three techniques for choosing inputs

You can't try every value, so choose inputs that represent many others.

Equivalence partitioning

Group inputs the system should treat the same way, and test one value from each group. For each field on this form there are at least two groups: empty, which should be rejected, and filled in, which should be accepted. Testing "Ada" and "Grace" separately adds nothing; they're in the same group.

Boundary value analysis

Bugs gather at the edges of groups: the shortest and longest allowed value, and the first value outside. For a name field, check a single character and a very long value. If a requirement states a maximum length, test exactly that length and one more. Our free boundary value generator lists the values for a field from its ranges, as 2-value or 3-value boundary analysis, and points out any values the ranges leave unspecified.

Error guessing

Use experience to try inputs that often break forms: spaces only, letters in a number field, accented characters and emoji, leading or trailing spaces, and pasted text. These don't come from a formula, which is why they find bugs formulas miss.

When settings combine: pairwise testing

The techniques above choose values for one field at a time. When several independent settings combine (browser, operating system, payment method, account type), testing every combination quickly becomes impossible. Pairwise testing covers every pair of values instead: four settings with three values each have 81 combinations, but every pair fits into 9 test cases. Our free pairwise test case generator builds that set, with rules for combinations that can't happen and export to CSV or a Gherkin Examples table. To see what such a set catches on this same checkout, and what it misses, read pairwise testing on Sauce Demo, measured against every combination.

Worked example: the checkout information form

These are the cases we designed and the results we observed:

ID Case Input (First / Last / Zip) Result observed
CHK-INFO-01 All fields valid Ada / Lovelace / 10115 Continued to "Checkout: Overview"
CHK-INFO-02 First name missing (empty) / Lovelace / 10115 "Error: First Name is required"
CHK-INFO-03 Last name missing Ada / (empty) / 10115 "Error: Last Name is required"
CHK-INFO-04 Postal code missing Ada / Lovelace / (empty) "Error: Postal Code is required"
CHK-INFO-05 Everything missing (empty) / (empty) / (empty) "Error: First Name is required" (only the first problem is shown)
CHK-INFO-06 Shortest values A / L / 1 Continued
CHK-INFO-07 Very long first name 300 × "A" / Lovelace / 10115 Continued; all 300 characters kept
CHK-INFO-08 Spaces only in first name 3 spaces / Lovelace / 10115 Continued
CHK-INFO-09 Letters in postal code Ada / Lovelace / ABCDE Continued
CHK-INFO-10 Accents and emoji Zoë 🙂 / Łukasiewicz / 00-950 Continued

Cases 02 to 04 come from equivalence partitioning (each field empty), 06 and 07 from boundary values, and 08 to 10 from error guessing.

What the results tell us

  • Cases 01 to 04 match the obvious expectations. Each required field is checked, and the messages name the field.
  • Case 05 shows only one error at a time. A user with three empty fields fixes one, clicks Continue, and sees the next error. That isn't necessarily a bug, but it's worth raising with the product owner, because it adds two extra round trips.
  • Cases 08 and 09 were accepted. A name made of spaces, and a postal code of letters, both passed. Whether that's a bug depends on a requirement the form doesn't state. Many countries have postal codes with letters ("SW1A 1AA" in the UK), so case 09 may well be correct. A name of only spaces almost certainly isn't.
  • Case 07 was accepted at 300 characters. Again there's no stated limit. The question to ask is what the order system downstream does with a 300-character name.

This is the real value of writing test cases carefully: they turn unstated assumptions into questions. Three of our ten cases produced a question, not a pass or a fail.

Write the expected result before you run the case

We ran the cases above to show real results. When you test your own product, fill in the expected column before running anything, from the requirements or from a conversation with the product owner. If you write it afterwards, you'll tend to describe whatever happened as correct.

When nobody can say what the expected result is, you've found a gap in the requirements. Record the question in the case and raise it; don't guess.

From test cases to automation

Cases 02 to 04 have the same steps and differ only in data, so they make one data-driven automated test. This Playwright version uses the logged-in shopper fixture from our Playwright tutorial:

import { test, expect } from "./fixtures";
 
const requiredFields = [
  { field: "First Name", message: "Error: First Name is required" },
  { field: "Last Name", message: "Error: Last Name is required" },
  { field: "Zip/Postal Code", message: "Error: Postal Code is required" },
];
 
for (const { field, message } of requiredFields) {
  test(`checkout requires ${field}`, async ({ shopper: page }) => {
    await page.getByRole("button", { name: "Add to cart" }).first().click();
    await page.getByTestId("shopping-cart-link").click();
    await page.getByRole("button", { name: "Checkout" }).click();
 
    const values = { "First Name": "Ada", "Last Name": "Lovelace", "Zip/Postal Code": "10115" };
    for (const [name, value] of Object.entries(values)) {
      if (name !== field) await page.getByRole("textbox", { name }).fill(value);
    }
    await page.getByRole("button", { name: "Continue" }).click();
 
    await expect(page.getByTestId("error")).toHaveText(message);
    await expect(page).toHaveURL(/checkout-step-one\.html$/);
  });
}

Each case becomes a separately named test in the report:

  ok 4 [chromium] › tests\shop\checkout.spec.ts:30:7 › checkout requires First Name (3.4s)
  ok 2 [chromium] › tests\shop\checkout.spec.ts:30:7 › checkout requires Last Name (2.7s)
  ok 6 [chromium] › tests\shop\checkout.spec.ts:30:7 › checkout requires Zip/Postal Code (2.5s)

Two details are worth copying:

  • The second assertion checks the URL. After the error, the page must still be the first checkout step. The error message alone doesn't prove the user was stopped.
  • .first() is acceptable here because these cases don't care which product is in the cart, only that there is one. When the product matters, find it by name; our UI automation mistakes article shows what .first() does after the list is re-sorted.

Not every case should be automated. The questions from cases 07 to 09 need an answer from a person first, and a case like "the error message is easy to understand" needs a person every time.

Common problems in test cases

  • Vague expected results. "Error is shown" passes for any error. Quote the message.
  • Several checks in one case. When "login, add item, checkout, logout" fails, you don't know which part failed. One case, one purpose.
  • Hidden preconditions. "Go to checkout" assumes a login and a full cart. Write them down.
  • Data that disappears. A case that depends on "order 1045" breaks when someone deletes it. Say how to create the data instead; see test data management.
  • Cases nobody updates. When the product changes, fix or delete the case in the same sprint. A wrong case is worse than no case, because people trust it.

Checklist

  • Every case has an ID, a one-line title, preconditions, steps, data, and an expected result.
  • Expected results are specific enough to fail, and were written before running the case.
  • Each field or rule has a valid and an invalid input (equivalence partitioning).
  • Limits are tested at the edge and just outside (boundary values).
  • At least a few cases come from error guessing: spaces, wrong types, special characters.
  • Unanswered questions are recorded in the case and raised, not guessed.
  • Cases with the same steps and different data are grouped, and automated together if they're automated.

Conclusion

Effective test cases aren't long; they're precise. Choose inputs with equivalence partitions, boundary values, and a bit of error guessing, write the expected result before you run anything, and treat an unclear result as a question for the team. On a form with only three fields, ten cases found three questions the requirements didn't answer. That's what good test design looks like in practice.

When a case does fail, the next step is a report someone can act on: how to write high-quality bug reports.

Sources and further reading

Tools mentioned

TestRailTest ManagementFree trial
QaseTest ManagementFree plan
ZephyrTest ManagementFree trial
JiraIssue TrackingFree plan
PlaywrightUI AutomationOpen source

Links go to each tool’s official site. How we choose and link tools

Revision history

Linked the boundary value generator.
Updated source links that had moved: the pages still exist, at new addresses.
Linked the hands-on pairwise testing article.
Rewritten with worked examples on the Sauce Demo checkout form, including the results we observed for each case and an automated version.
Removed a case study we could not verify.
Revised during a site-wide content audit.
First published.

Spotted a mistake? Report it — corrections land here.

Written and reviewed by

QA Vibes Editorial

Articles are written and reviewed by practicing QA and automation engineers. Every article lists its sources and shows when it was last updated.

Design your own cases

Boundary value generator

Enter a field's ranges, and get its partitions, boundary values, and the gaps nobody specified.