Equivalence Partitioning
The overview framed the whole part with one uncomfortable fact: you cannot test every input, because for almost any real field there are far more possible values than you could ever run. A 32-bit integer field alone has over four billion values. Testing them one by one is not “thorough” — it is impossible. So the first and most fundamental test-design technique is not about testing more. It is about testing smarter: proving that most of those four billion values are redundant, and that a handful of carefully chosen ones cover them all.
That technique is equivalence partitioning. It is the foundation the rest of this part builds on — boundary value analysis refines it, decision tables combine it across inputs, and every experienced tester reaches for it before writing a single case. Get this one idea right and the infinite input space stops being frightening. Get it wrong and you will test thousands of values while quietly missing the bug.
The core idea: inputs the program treats the same
Section titled “The core idea: inputs the program treats the same”An equivalence class (or equivalence partition) is a set of inputs the program is supposed to handle identically. If the specification says “ages 18 to 65 are eligible,” then 18, 19, 40, and 65 are all in one class — the program should give every one of them the same answer: eligible. From the program’s point of view they are interchangeable.
Here is the leap that makes the technique work. If the program genuinely treats every value in a class the same way, then any one value stands in for all of them. Testing 40 tests the entire eligible-age class, because 40 cannot succeed while 19 fails unless the program does not, in fact, treat them the same — which is precisely the bug you were hoping to find, and which one representative will surface just as well as a thousand.
Input space for "age" (0 .. 150+, effectively unbounded)
┌──────────────┬───────────────────────┬──────────────┐ │ below range │ in range │ above range │ │ 0 .. 17 │ 18 .. 65 │ 66 .. │ │ (ineligible) │ (eligible) │ (ineligible) │ └──────────────┴───────────────────────┴──────────────┘ ▲ ▲ ▲ pick one pick one pick one e.g. 10 e.g. 40 e.g. 80Three classes, three representatives. You have replaced an unbounded number of test values with three, and — if the partition is correct — lost no coverage.
Valid and invalid classes
Section titled “Valid and invalid classes”A common beginner mistake is to partition only the inputs the program should accept. But a program is defined as much by what it rejects as by what it accepts, and rejection logic has bugs too. So every equivalence partition splits into two kinds of class:
- Valid classes — inputs the program should accept and process normally. The eligible ages
18..65above. - Invalid classes — inputs the program should reject, reject gracefully, and reject with the right error. Ages below 18, ages above 65, but also the non-numeric, the negative, the empty, the absurdly large.
Invalid classes are where crashes live. “What happens when someone types -5, or abc, or leaves the field blank, or enters 999999999999?” Each of those is a distinct way to be invalid, and each may be handled by different code — so each is its own class. A robust partition names the invalid classes explicitly rather than lumping them into a vague “bad input.”
Field: "quantity to order" (spec: integer, 1 .. 999)
VALID C1: 1 .. 999 rep: 50
INVALID C2: below range (<= 0) rep: 0 C3: above range (>= 1000) rep: 1000 C4: non-integer text rep: "abc" C5: empty / absent rep: ""Five classes, five representatives, five tests. Notice that C1 is the only class the “happy path” would have covered. The other four are exactly the cases that ship broken when a team tests only what should work.
A complete, disjoint partition
Section titled “A complete, disjoint partition”The word partition is doing real mathematical work here, and it is worth honouring precisely. A true partition of the input space has two properties, and both matter:
- Complete (exhaustive). Every possible input falls into some class. There is no value you could type that belongs to no class — if there is, you have found a case nobody thought about, which is a fine way to find bugs but a poor way to have designed the test.
- Disjoint (non-overlapping). Every input falls into exactly one class. No value belongs to two classes at once. If a value could be in two, your classes are not really “treated the same” — the program has to pick one behaviour, and the overlap hides which.
Complete + disjoint = every input has exactly one home
✓ ├──C1──┼──C2──┼──C3──┤ no gaps, no overlaps
✗ ├──C1──┼─C2─┐ ┌─C3──┤ GAP: some inputs unclassified └─┘ ✗ ├──C1──┼──C2──┤ OVERLAP: this input is in └────C3────┤ both C2 and C3Completeness protects you from missing a behaviour; disjointness protects you from ambiguity about which behaviour applies. When you sketch your classes, run both checks: “Is there any input with no class?” and “Is there any input with two?” If both answers are no, the partition is sound.
How to actually find the classes
Section titled “How to actually find the classes”Partitioning feels obvious in a textbook example and slippery on a real form. A repeatable procedure keeps it honest:
- Read the specification for every distinct outcome. Each different result the program can produce — accepted, rejected-too-low, rejected-too-high, rejected-malformed — implies at least one class. Outcomes are the seams the input space breaks along.
- For each input field, list its ranges and its formats. A numeric field partitions by range (below / in / above). A structured field (email, date, code) partitions by shape as well — well-formed versus each way it can be malformed.
- Add the invalid classes the spec forgot to mention. Empty, absent, negative, over-long, wrong type. Requirements describe the happy path; the robustness comes from you naming the unhappy ones.
- Check completeness and disjointness. Sweep the number line and the format space: is there any value with no home, or any with two? Fix the partition, not the tests.
- Pick one representative per class — near its edge, not its middle. A middle value is comfortable and proves the least. This is the hand-off to boundary analysis, which formalises where in the class to stand.
Do this on paper before you write a single test, and the tests almost write themselves: one per class, with its expected outcome already decided.
Worked example: partitioning an age field
Section titled “Worked example: partitioning an age field”Let us make this concrete with a rule you have surely met: an account can only be opened by someone aged 18 to 65 inclusive. We will partition the age input completely and disjointly, then pick one representative each.
Requirement: eligible if 18 <= age <= 65
┌─────────────┬────────────────────┬─────────────┐ │ C1 below │ C2 in range │ C3 above │ │ age < 18 │ 18 <= age <= 65 │ age > 65 │ │ reject │ accept │ reject │ └─────────────┴────────────────────┴─────────────┘ rep: 10 rep: 40 rep: 80
plus invalid-format classes the number line does not show: C4 negative rep: -1 C5 non-numeric rep: "forty" C6 empty / absent rep: ""The three number-line classes are complete and disjoint over the integers: pick any age and it lands in exactly one of below / in / above. The three format classes cover the ways the input is not even a valid age. Six classes, six tests:
| Class | Representative | Expected behaviour |
|---|---|---|
| C1 below range | 10 | reject — under minimum |
| C2 in range | 40 | accept — eligible |
| C3 above range | 80 | reject — over maximum |
| C4 negative | -1 | reject — invalid age |
| C5 non-numeric | "forty" | reject — not a number |
| C6 empty / absent | "" | reject — required field |
Compare the effort. The naive approach tests “lots of ages” — say every integer from 0 to 100 — and runs 101 cases while still never trying -1, "forty", or "". Partitioning runs six cases and covers strictly more behaviour. Fewer tests, better coverage: that is the whole promise, and it is not a trick, it is a consequence of the equivalence assumption being true.
Here is the same idea as an executable check. The function under test is deliberately buggy — it forgets the upper bound entirely, which means the whole C3 (above-range) class is mishandled:
def is_eligible(age): return age >= 18 # BUG: upper bound (<= 65) is missing
# One representative per class — six assertions, not six billion.assert is_eligible(10) is False # C1 below -> passesassert is_eligible(40) is True # C2 in range -> passesassert is_eligible(80) is False # C3 above -> FAILS: 80 wrongly eligible# C4-C6 (negative, non-numeric, empty) would be caught by input parsingThe C3 representative (80) fails and exposes the missing upper bound. One value stood in for the entire above-range class — and it was enough.
The risk: a bad representative hides a bug
Section titled “The risk: a bad representative hides a bug”Everything above rests on a single assumption: that the class is genuinely uniform — that the program really does treat every member the same. When that assumption holds, one representative is as good as all of them. When it does not hold, partitioning can lull you into false confidence, and this is the technique’s core risk.
Suppose your “in range” age class is 18..65, and internally the code has a hidden seam you did not know about — say, a separate discount path that only triggers at exactly 65. You pick 40 as your representative. It passes. You declare the whole class covered. But 65, a sibling in the same class, hits the buggy path and would have failed. Your representative passed while its sibling fails, and because you tested only the representative, the bug ships.
Class C2 you drew: 18 ........ 40 ........ 65You believe: all treated identicallyReality (hidden seam): 18..64 one path │ 65 another pathYour representative: 40 (safe path) -> PASSThe sibling that fails: 65 (buggy path)
A wrong partition hid a real bug behind a passing test.The defect is not in the technique; it is in the partition. You drew a class that was not actually uniform, so its representative did not truly represent it. This is why partitioning is never the last word: you must choose representatives with suspicion, and you must test the edges of every class, because edges are exactly where a class most often turns out to be secretly non-uniform. That suspicion is the entire subject of the next page.
Under the hood — how a representative can lie
Section titled “Under the hood — how a representative can lie”The uniformity assumption breaks in predictable places, and knowing them tells you where a class needs more than one representative:
- Hidden internal boundaries. A class that looks uniform in the spec may straddle a code branch (
if age == 65), a type limit, or a caching threshold. The representative sails through the common branch and never touches the rare one. - Type edges inside the class. A “valid integer” class silently contains
0,MAX_INT, and the sign flip.50exercises none of them. - Empty and singleton shapes. A “list of items” class contains the empty list and the one-element list, which often run different code (a loop that never iterates, an index that assumes a neighbour). A three-element representative hides both.
The mitigation is the same in every case: pick representatives near the edges of the class, not comfortably in its middle — and where a class plausibly hides a seam, split it and test more than one point. That instinct is precisely what boundary value analysis systematises.
Where this leaves us
Section titled “Where this leaves us”Equivalence partitioning is the load-bearing technique of test design. It answers the question the overview posed — how do you choose test cases with intent when you cannot try everything? — with a single, defensible move: group inputs the program should treat identically, then test one representative per group. Everything downstream in this part assumes you have already done it. Decision tables partition combinations of inputs; state-transition testing partitions sequences; pairwise testing samples across many partitioned fields at once.
But we have also seen its Achilles’ heel: a class is only as trustworthy as its uniformity, and real code hides seams inside classes that look uniform on paper. The representative in the middle is the one most likely to lie. The direct answer to that weakness — go stand at the edge of every class, where non-uniformity concentrates — is boundary value analysis, the very next page and the natural completion of this one. Partitioning finds the classes; boundaries interrogate their seams. Neither is complete without the other.
The architect’s lens
Section titled “The architect’s lens”- Why does it exist? Because the input space of almost any field is effectively infinite, and exhaustive testing is impossible. Partitioning exists to prove that most inputs are redundant, so a finite, small suite can cover an unbounded space.
- What problem does it solve? It turns “which of these billions of values do I test?” into “how many distinct behaviours are there?” — collapsing an intractable input space into a handful of classes and reducing test count from N-billion to roughly N.
- What are the trade-offs? The whole method is only as good as the partition. A well-drawn partition gives huge coverage for tiny effort; a wrongly drawn one gives false confidence, because a passing representative can mask a failing sibling. It also assumes intra-class uniformity that real code does not always honour.
- When should I avoid it? When inputs are not independent — when the behaviour depends on combinations of several fields rather than each field’s class in isolation. There, reach for decision tables or pairwise testing instead. And never use it alone, without boundary analysis.
- What breaks if I remove it? You lose the principled basis for choosing test cases at all. Testing degenerates into either “try a few values that come to mind” (arbitrary, gap-ridden) or “try everything” (impossible). Every later technique in this part assumes you have already partitioned; remove it and they have no edges to refine.
Check your understanding
Section titled “Check your understanding”- In one sentence, why can testing a single representative stand in for an entire equivalence class — and what assumption makes that valid?
- A partition must be complete and disjoint. Define each property and name the specific failure each one prevents.
- Partition a “quantity” field specified as an integer from 1 to 999. List the valid and invalid classes and give one representative per class.
- Testing an integer field with partitioning needed 6 tests instead of ~4 billion. State the general rule this illustrates about how test count grows with the number of classes.
- Explain the core risk of partitioning using the idea of a “passing representative and a failing sibling,” and name the technique that most directly mitigates it.
Show answers
- Because a class is defined as inputs the program should treat identically, so any one member behaves like all the others — the valid assumption is intra-class uniformity: that the program genuinely handles every value in the class the same way. If that holds, one representative cannot pass while a sibling fails.
- Complete (exhaustive): every possible input falls into some class — prevents missing a behaviour (an input nobody classified and therefore nobody tested). Disjoint (non-overlapping): every input falls into exactly one class — prevents ambiguity about which behaviour applies to an input that is in two classes at once.
- Valid:
C11..999 (rep50). Invalid:C2below range,<= 0(rep0);C3above range,>= 1000(rep1000);C4non-integer text (rep"abc");C5empty/absent (rep""). Five classes, five representatives. (Naming extra invalid classes such as “negative” separately is also acceptable.) - N classes need about N tests, not N-billion. Test count grows linearly with the number of distinct classes, not with the size of the input space — each new class adds exactly one test, so partitioning scales to forms with many fields.
- If a class is not truly uniform (it hides an internal seam or type edge), the representative you picked may run the safe path and pass, while a sibling in the same class runs the buggy path and would fail — but because you only tested the representative, the bug ships. The most direct mitigation is boundary value analysis, which tests the edges of each class where non-uniformity most often hides.