Test Design: Choosing What to Check
Part 5 of the Software Testing Course: the techniques for designing a single test well — equivalence partitioning, boundary values, decision tables, state transitions, and error guessing.
Part 4 was about how many tests to write at each layer. This part is about making each one count. You can't check every possible input, so test design is the craft of choosing a small set of checks that catches the most bugs per test written.
The problem: you can't test everything
A form field that accepts an integer from 1 to 1,000,000 has a million valid inputs and infinitely many invalid ones — negative numbers, decimals, letters, an empty string, a value with a leading zero, a number in scientific notation. Testing them all is impossible. Testing three at random is close to useless, because random values tell you nothing about the edges, which is where the bugs are.
Good test design solves this by picking inputs that each stand in for a whole category of behaviour. If you choose the categories well, a handful of tests covers the meaningful risk.
Equivalence partitioning
Divide the input space into groups — "partitions" — where the software should behave identically, then test one representative value from each.
For a rule like "age must be between 18 and 65":
- Below range: anything under 18 → rejected.
- In range: 18 through 65 → accepted.
- Above range: anything over 65 → rejected.
Three tests — say 15, 40, 70 — cover the meaningful behaviour. A fourth test at 41 tells you almost nothing that the test at 40 didn't, because 40 and 41 are in the same partition. The skill is spotting all the partitions: for a text field that might be empty, valid, too long, contains disallowed characters, and contains only whitespace.
Boundary value analysis
Bugs cluster at the edges of partitions, because that's where the code makes a comparison and a < gets written where <= was meant. So test the boundaries explicitly.
For "18 to 65": test 17, 18, 65, 66 — the last rejected and first accepted value at each edge (17/18 at the low end, 65/66 at the high end). Those four catch the off-by-one mistakes that 15 and 70 sail right past. Boundaries and partitions are almost always used together: pick one value from the middle of each partition, plus the values on either side of every boundary.
Boundaries aren't only numeric. The first and last item in a list, an empty collection versus one with a single element, the maximum length of a string field, the moment a session expires — all are boundaries worth a specific test.
Decision tables
When behaviour depends on a combination of conditions, a decision table lays out every combination so you can't silently miss one.
| Logged in | Has active subscription | Feature flag on | Result |
|---|---|---|---|
| No | — | — | Redirect to login |
| Yes | No | — | Show upgrade prompt |
| Yes | Yes | No | Show "coming soon" |
| Yes | Yes | Yes | Show the feature |
Each row is a test. Building the table is the valuable part — it forces you to enumerate the conditions and confront combinations nobody discussed, like "subscribed but the flag is off." If a table has too many rows to test all of them, prune with judgement (some combinations are impossible or equivalent), but do it consciously.
State transition testing
When the system moves between states, test the transitions — and specifically test the ones that shouldn't be allowed.
A document that moves draft → in review → published → archived:
- Test each valid transition works.
- Test that an invalid transition is rejected: draft → published directly, archived → in review, published → draft by a user without permission.
Invalid transitions are where security and data-integrity bugs live — a user editing a published document because the "can edit" check only considered the draft state, or an order being cancelled after it shipped.
Error guessing and exploratory input
Structured techniques cover the specified behaviour. Experience covers the rest. Keep a standing checklist of inputs that have caused bugs before:
- Empty input, and input that's only whitespace.
- Extremely long strings — 10,000 characters in a name field.
- Emoji, right-to-left text, and other Unicode.
- Pasting instead of typing (paste often skips input-event validation).
- Double-clicking submit, or submitting with the Enter key.
- Using the browser back button mid-flow, then resubmitting.
- A session that expires between loading a form and submitting it.
- Two browser tabs acting on the same record.
This is closer to exploratory testing than to a technique, but a written checklist makes it repeatable.
How this applies to autonomous testing
Autonomous exploration handles the breadth — reaching flows and states you didn't think to script. But the specific assertions still benefit from these techniques. When you write a plain-English check for a critical flow, design its inputs the same way you'd design a scripted test: pick a boundary value rather than a random one, walk the decision-table rows, name the exact expected result rather than "it works." A plain-English check that says "enter an age and submit" is weaker than one that says "enter 17 and confirm it's rejected; enter 18 and confirm it's accepted."
The takeaway
You can't test everything, so test representatives: one value from the middle of each equivalence partition, the values on both sides of every boundary, every row of the decision table, every state transition (valid and invalid), plus a standing checklist of nasty inputs. These five techniques turn "what should I check?" from guesswork into a method. Part 6 moves on to exploratory testing as its own discipline.