Overview — Choosing Test Cases with Intent
Every earlier Part of this book has circled one question: how do you earn justified confidence that software works? The tester’s mindset taught you to hunt for where things break; the levels and types taught you where in the system to look. This Part answers the question that comes next and is harder than it sounds: given a specific function, screen, or API, which exact inputs do you actually run?
That is the discipline of test design. It sits between “I should test this” and “here is the test I wrote,” and it is where most of the leverage in testing lives. Choose your cases well and a dozen tests can flush out bugs that ten thousand random inputs would miss. Choose them badly — or at random — and you can run tests all day and still ship the defect that takes production down.
You cannot test everything
Section titled “You cannot test everything”Start with the fact that makes test design necessary at all: the input space of almost any real program is, for practical purposes, infinite.
Take a function that adds two 32-bit integers. That is about as simple as software gets. Yet each argument has 2³² possible values, so the pair has 2³² × 2³² ≈ 1.8 × 10¹⁹ combinations. At a billion test cases per second it would take roughly 584 years to run them all — for addition. Add a third parameter, a string, or a timestamp, and the number stops being large and becomes meaningless.
If you accept that number, one conclusion is forced on you: testing is sampling. You will only ever run a vanishingly small fraction of the possible inputs. The entire question of test design is which fraction — and that is a question you get to answer well or badly.
The core problem: find the edge cases without testing every case
Section titled “The core problem: find the edge cases without testing every case”Restated sharply, the job is this:
Find the inputs that break the software without running every input.
Two facts make this tractable. First, bugs are not spread evenly across the input space. They cluster — at the edges of ranges, at the transitions between states, at the surprising combinations of options, at the values a programmer forgot existed. Second, huge swathes of the input space are redundant: for a given branch of logic, thousands of inputs are treated identically, so one of them tests the branch as well as all of them.
Put those two facts together and a strategy appears. Skip the redundant middle. Aim deliberately at the seams where bugs pile up. That is what every technique in this Part is a concrete, repeatable way to do.
Why random selection wastes effort
Section titled “Why random selection wastes effort”A natural first instinct is: if I can’t test everything, I’ll test a random handful. Random inputs feel unbiased and easy to generate. But as a strategy for finding bugs, random selection is quietly wasteful.
input axis for "age >= 18" (bug lives exactly at 18)
0 18 130 |---------|--------------------------------------------| reject | accept ^ the ONE value that distinguishes age > 18 from age >= 18
random darts: x x x x x x x x (almost never land on 18 — the off-by-one survives)If the defect is an off-by-one at age 18, a random integer between 0 and 130 hits it with probability roughly 1 in 131. You could run hundreds of random cases and miss it. Worse, random selection gives you no idea what you covered: after a hundred passes you cannot say which behaviours you exercised or which you skipped, so you cannot say when to stop. Effort spent without coverage you can name is effort you cannot bank as confidence.
Systematic test design fixes both problems. It chooses cases with intent — each case exists to exercise a specific behaviour or catch a specific class of bug — and it comes with known coverage, so you can point at what you tested and defend the claim. (Random and generated inputs are not worthless, by the way; property-based and fuzz testing put them to excellent use. That is a complement to the designed cases here, covered later in types of testing.)
Two families of techniques
Section titled “Two families of techniques”The techniques in this Part fall into two families, distinguished by what they reason about.
Input-space techniques reason about the values a program accepts. They ask: how do I carve an infinite set of inputs into a small set of representatives that still covers everything that matters? Equivalence partitioning, boundary value analysis, and pairwise/combinatorial testing all live here.
Behavior techniques reason about the program’s logic and lifecycle. They ask: given these rules or these states, what combinations and sequences must I exercise? Decision tables (for tangled if/else rules) and state-transition testing (for anything with modes, sessions, or workflows) live here.
TEST DESIGN | +-------------------+--------------------+ | | INPUT-SPACE techniques BEHAVIOR techniques "which values?" "which logic / sequences?" | | equivalence partitioning decision tables boundary value analysis state-transition testing pairwise / combinatorial | | +--------------- error guessing ---------+ (experience aimed at both)Most of the two families are not either/or. Real testing weaves them together, which is why this Part ends with a page on combining techniques rather than declaring a winner.
The roadmap for this Part
Section titled “The roadmap for this Part”Read these in order. Each page teaches one technique, shows a worked example, and names the specific kind of bug it is built to catch.
| # | Page | Technique it teaches | The bug it catches |
|---|---|---|---|
| 2 | Equivalence Partitioning | Split inputs into classes that behave alike; test one per class | Whole categories of input silently mishandled (e.g. negatives, empties) |
| 3 | Boundary Value Analysis | Test values on and around the edges of each class | Off-by-one, wrong operator (< vs <=), overflow at the limit |
| 4 | Decision Tables | Enumerate combinations of conditions and their required actions | Missing or contradictory rules in tangled business logic |
| 5 | State-Transition Testing | Model states and events; test valid and invalid transitions | Illegal transitions, stuck states, actions allowed in the wrong mode |
| 6 | Pairwise and Combinatorial Testing | Cover every pair of option values without the full cross-product | Bugs triggered by a specific combination of settings/flags |
| 7 | Error Guessing and Experience-Based Testing | Aim tests using known failure patterns and intuition | The weird inputs specs forget: nulls, huge values, emoji, leap days |
| 8 | Combining Techniques in Practice | Layer the techniques on one real feature, budgeting effort | Gaps left when any single technique is used alone |
| 900 | Revision — Test Design Techniques Recap | Consolidates the whole Part | — |
The thread
Section titled “The thread”Here is the one idea to carry through every page that follows. Confidence is not earned by quantity of tests — a million random cases prove almost nothing you can name. It is earned by testing the places where software is most likely to break, and being able to say, precisely, which of those places you covered.
Each technique in this Part is a lens on “where does this break?”: partitioning finds forgotten categories, boundary analysis finds the edges, decision tables find the missing rules, state machines find the illegal sequences, pairwise finds the toxic combinations, and error guessing finds what the spec never imagined. Test design is how you turn the mindset from Part one — reason about where things break — into a small, deliberate, defensible set of cases.
Check your understanding
Section titled “Check your understanding”- Roughly how long would it take to exhaustively test a two-argument 32-bit integer function at a billion cases per second, and what conclusion does that force about testing in general?
- State the core problem of test design in one sentence, using the words edge cases and without.
- Give two concrete reasons random test selection is a poor strategy for finding known bug patterns.
- Name the two families of techniques in this Part and the different question each one reasons about.
- Using the book’s throughline, explain why “we ran a million random inputs” is weaker evidence of correctness than “we tested every boundary and every equivalence class.”
Show answers
- About 584 years (2³² × 2³² ≈ 1.8 × 10¹⁹ combinations at 10⁹/s). It forces the conclusion that exhaustive testing is impossible even for trivial code, so all testing is sampling — the only question is which sample.
- Test design is finding the edge cases that break software without running every possible input.
- First, random inputs rarely land on the specific values where bugs cluster (an off-by-one at a single boundary is almost never hit by chance). Second, random selection gives you no known coverage — you can’t say what you exercised, so you can’t say when you’re done or defend the confidence.
- Input-space techniques (equivalence partitioning, boundary value analysis, pairwise/combinatorial) reason about which values to feed in. Behavior techniques (decision tables, state-transition testing) reason about which logic paths and event sequences to exercise.
- Confidence comes from covering where software is most likely to break, and from being able to name what you covered. A million random inputs prove almost nothing you can point to; a set of designed cases with known coverage lets you say exactly which categories, edges, rules, and transitions you exercised — that nameable coverage is what turns testing into justified confidence.