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.