Combining Techniques in Practice
The last six pages each handed you a lens. Equivalence partitioning collapses an infinite input space into a few classes. Boundary value analysis sharpens the edges of those classes. Decision tables untangle combinational logic. State-transition testing covers behaviour that depends on history. Pairwise tames an explosion of independent parameters. And error guessing fills the gaps the systematic techniques leave behind.
Here is the thing nobody tells you until you’ve shipped a few features: none of these techniques is a strategy on its own. A real feature has a bit of everything — a range here, a combination there, a few independent knobs, a boundary that will hurt if you get it wrong. Point one technique at it and you cover one shape of input while missing the others. This page is about the move that turns a toolkit into a suite: matching each technique to the shape of the input, then layering them on one feature so their coverage stacks instead of overlaps wastefully.
The throughline of the whole book lands hardest here. Intentional test selection earns justified confidence cheaply — but only if you pick the right tool for each part of the feature and can say, out loud, what every test is for.
A decision guide: match the technique to the shape of the input
Section titled “A decision guide: match the technique to the shape of the input”Before you write a single case, look at the feature and ask what shape each input has. The shape tells you the technique. This is the map:
Shape of the input Reach for────────────────────────────────────────────────────────────────An ordered range or continuum ──► Partitioning + BVA(age, price, length, dates) (classes, then edges)
Output depends on a COMBINATION of ──► Decision tableseveral conditions (if A and B but C…) (rules → cases)
Behaviour depends on HISTORY / ──► State-transitionwhat happened before (a workflow) (states, events, edges)
MANY independent parameters, each ──► Pairwise / combinatorialwith a few values, no shared logic (cover all value-pairs)
"What weird thing could break this?" ──► Error guessing(the gaps the above miss) (experience + attack)None of these are mutually exclusive. A single feature usually has two or three shapes at once — a range and a combination and a pile of parameters. The skill is not picking one technique; it is decomposing the feature into its shapes and applying the matching technique to each. Do that and coverage becomes deliberate: every test traces back to a shape, and every shape traces back to a technique.
The worked feature: a shipping-cost calculator
Section titled “The worked feature: a shipping-cost calculator”Let’s make this concrete with one feature and drive every technique through it. The feature is a shipping-cost estimator for an online store. Its signature:
estimate_shipping( weight_kg, # 0.0 < weight <= 30.0, else rejected destination, # "domestic" | "eu" | "international" speed, # "standard" | "express" membership, # "none" | "plus" | "prime" is_gift # bool — adds gift wrapping) -> price | RejectedThe business rules:
- Weight above 30 kg or at/below 0 is rejected — parcel service won’t carry it.
- Base cost depends on
destinationandweight. expressadds a surcharge;international+expressis only offered forplus/primemembers (otherwise rejected).primemembers get free standard domestic shipping;plusmembers get 50% off standard shipping.is_giftadds a flat wrapping fee, always.
Read those rules again and you can already see the shapes: weight is a range. The destination × speed × membership interaction is a combination. And once you add is_gift and the membership tiers, you have several independent-ish parameters. That is the decomposition. Now we layer.
Layer 1 — Partition the range inputs
Section titled “Layer 1 — Partition the range inputs”Start with the one continuous input: weight_kg. Partitioning gives us classes of uniform behaviour:
weight_kg partitions────────────────────────────────────────────── (-∞, 0] invalid — rejected (0, 30] valid — priced (30, +∞) invalid — rejectedOne representative from each class — say -2, 12.5, 45 — proves the calculator accepts the valid band and rejects outside it. Three cases, and the whole continuum of weights is represented. That is partitioning doing its job: breadth for almost nothing.
But partitioning quietly assumes every value inside (0, 30] behaves the same. We know from the previous pages that the assumption breaks exactly at the edges — so we layer BVA on top.
Layer 2 — Boundary-test the edges
Section titled “Layer 2 — Boundary-test the edges”The valid band (0, 30] has two edges: a lower edge at 0 (exclusive) and an upper edge at 30 (inclusive). Apply the three-value approach at each, respecting the type’s granularity — weight is a decimal, so “just above/below” moves by the smallest meaningful step (here, 0.01 kg):
lower edge (0, exclusive): 0.00 (reject) 0.01 (accept) ...upper edge (30, inclusive): 29.99 (accept) 30.00 (accept) 30.01 (reject)That yields the boundary values 0.00, 0.01, 29.99, 30.00, 30.01. Each one targets a specific bug the middle value 12.5 can never reveal:
| Value | Targets |
|---|---|
0.00 | weight > 0 written as weight >= 0 — accepting a zero-weight parcel |
0.01 | confirms the valid band genuinely opens just above zero |
29.99 | confirms the band holds near the top |
30.00 | weight < 30 off-by-one that rejects a valid 30 kg parcel |
30.01 | weight <= 30 written as <= 31 — accepting an over-limit parcel |
Layers 1 and 2 together handle the range shape completely: partitioning found the edges, BVA hit them precisely. Neither alone was enough — partitioning would ship the off-by-one at 30.00; BVA alone had no principled way to know where 30 even was.
Layer 3 — Table the combinational logic
Section titled “Layer 3 — Table the combinational logic”Now the tangle: the rules where the price depends on a combination of destination, speed, and membership. Prose hides contradictions; a decision table exposes them. We build one for the trickiest cluster — whether the order is even offered, and which discount applies:
Conditions R1 R2 R3 R4 R5 R6────────────────────────────────────────────────────────────────────destination = international Y Y Y - - -speed = express Y Y N - - -membership in {plus, prime} N Y - - - -destination = domestic - - - Y Y Yspeed = standard - - - Y Y Ymembership = prime - - - Y N Nmembership = plus - - - - Y N────────────────────────────────────────────────────────────────────Actions Reject (not offered) X Price: intl express X Price: intl standard X Price: free (prime domestic std) X Price: 50% off (plus domestic std) X Price: full standard XEach column is one test case, and each is justified by a rule, not by intuition. Rule R1 alone catches the “international express for a non-member” rejection — a case a careless suite would forget because it sits at the intersection of three conditions. The table also earns its keep before any test runs: filling it in is when you discover the spec never said what happens for international + standard + prime. Ambiguity surfaced at design time is a bug caught for free.
Layer 4 — Pairwise the remaining parameters
Section titled “Layer 4 — Pairwise the remaining parameters”We still haven’t systematically exercised the interaction between all the knobs — including is_gift, which is independent of everything else. Testing every combination is the explosion pairwise exists to defuse:
weight-class (3) × destination (3) × speed (2) × membership (3) × is_gift (2)= 3 × 3 × 2 × 3 × 2 = 108 full combinations108 cases for one estimator is more than the feature is worth. Pairwise cuts it down by covering every pair of values at least once — enough to catch any bug that needs just two parameters to interact, which empirically is the overwhelming majority. A pairwise generator reduces those 108 to roughly 9–12 cases:
# | weight | destination | speed | membership | is_gift──┼────────┼───────────────┼──────────┼────────────┼────────1 | valid | domestic | standard | none | false2 | valid | eu | express | plus | true3 | valid | international | express | prime | false4 | low | domestic | express | prime | true5 | high | eu | standard | none | false6 | valid | international | standard | plus | false7 | valid | eu | standard | prime | true8 | valid | domestic | express | none | true9 | valid | international | express | none | false ← reject...Every value of every parameter appears, and every pair (say international + express, or prime + is_gift=true) shows up in at least one row. Row 9 even re-catches the R1 rejection from the decision table — which brings us to the interesting part.
Where the techniques overlap — and where each uniquely earns its place
Section titled “Where the techniques overlap — and where each uniquely earns its place”You may have noticed the layers touch the same feature more than once. Row 9 of the pairwise set re-checks a rejection the decision table already covered. That overlap is not waste — it is defence in depth — but it does raise the honest question: does every technique earn its place, or are some redundant?
Here is the audit, per shape:
- Partitioning uniquely guarantees no class of weight is unrepresented. Pairwise picks weight values too, but only as a parameter — it has no notion of “the invalid-low class must appear.” Drop partitioning and you might test three valid weights and zero rejections.
- BVA uniquely hits
30.00versus30.01. No other technique here even looks at that granularity — pairwise treats weight as three coarse buckets. Drop BVA and the off-by-one ships. - The decision table uniquely proves the logic is right for each rule, including the “international + express + non-member = reject” intersection. Pairwise might stumble onto that combination, but it doesn’t guarantee the three-way condition, and it can’t tell you the resulting price is correct per rule.
- Pairwise uniquely covers interactions the table ignores — e.g.
is_giftcombined with every membership tier — cheaply, without you enumerating them by hand.
range combination interaction edges of conditions of many params │ │ │ Partition+BVA Decision table Pairwise └────────────────┴────────────────┘ covers overlapping middle, but each owns a corner nothing else doesThe overlap is in the middle of the diagram; the value is in the corners. Remove any one technique and a corner of the feature goes untested — a corner that, by the logic of each earlier page, is exactly where a bug likes to live.
Prioritizing when time is limited
Section titled “Prioritizing when time is limited”You will rarely get to write all 24 cases before a deadline. So layering comes with a priority order, driven by risk — the product of likelihood a bug lives here and cost if it ships:
- Highest-risk boundaries first. The
30.00upper edge and the0.00lower edge. Off-by-ones are the most common defect and the boundary is where they hide. If money or safety rides on the edge, it is case number one. - High-risk combinations next. The decision-table rules that reject or charge differently — R1 (international express non-member reject) and R4 (prime free shipping). A wrong price is a direct revenue or trust bug.
- One representative per partition. Cheap breadth so no whole class is dark.
12.5 kg, a rejection, and a normal happy path. - Pairwise interactions, as budget allows. Add rows until you run out of time; because each row covers many new pairs, the coverage curve is steep early and flat late — so even a truncated pairwise set is worth a lot.
- Error guessing on whatever’s left.
weight = NaN,destination = "Domestic"(wrong case), a negative-zero, an emoji in a field. The experience-based net over the systematic gaps.
The rule of thumb: spend the first hour on the edges and the combinations that cost the most when wrong. Everything else is a lower-risk addition to an already-defensible core.
Make the coverage explicit
Section titled “Make the coverage explicit”The final discipline is what separates a suite you can defend from one that merely exists: state what each test targets. A suite is auditable only if a reviewer can read it and see the intent behind every case. In practice that means naming and commenting tests after the shape and technique they serve. Here is the shipping suite expressed as runnable checks, each annotated with its job:
# --- Layer 1: partitioning (weight classes) ---assert estimate(-2, "domestic", "standard", "none", False) is REJECTED # invalid-low classassert estimate(12.5, "domestic", "standard", "none", False) == price # valid classassert estimate(45, "domestic", "standard", "none", False) is REJECTED # invalid-high class
# --- Layer 2: BVA (upper edge of valid band) ---assert estimate(29.99, "domestic", "standard", "none", False) != REJECTED # just below 30assert estimate(30.00, "domestic", "standard", "none", False) != REJECTED # ON edge (off-by-one)assert estimate(30.01, "domestic", "standard", "none", False) is REJECTED # just above 30
# --- Layer 3: decision table (rule R1 — intl express non-member) ---assert estimate(5, "international", "express", "none", False) is REJECTED # R1: not offered
# --- Layer 3: decision table (rule R4 — prime free domestic standard) ---assert estimate(5, "domestic", "standard", "prime", False) == 0.0 # R4: free shipping
# --- Layer 4: pairwise (is_gift interacts with prime discount) ---assert estimate(5, "domestic", "standard", "prime", True) == GIFT_FEE # free + wrapRead top to bottom, this suite narrates its own coverage. A teammate can see that the range is partitioned, the upper edge is boundary-tested, the rejection and discount rules are individually justified, and the gift-wrap interaction is exercised. Nothing is accidental; every line answers “what would break if this were removed?” with a specific bug. That is what “auditable, not accidental” means — coverage you can point at, not coverage you hope you have.
Compare this to a suite of ten checks all near 12.5 kg, domestic, standard, none — comfortable middle values that pass no matter how the logic bends. It has more green checkmarks and less confidence, because the checkmarks aren’t aimed anywhere. The layered suite has fewer cases and more confidence, because each case is aimed at a place bugs live.
The throughline
Section titled “The throughline”Every technique in this Part is a way of spending a limited test budget where defects concentrate rather than where they are convenient. Combining them is the same idea one level up: decompose a feature into its shapes, apply the matching technique to each, layer them so their coverage stacks, prioritize the highest-risk edges and combinations first, and annotate every test with its intent.
Do that and you get the promise the book keeps making — justified confidence earned cheaply. Not confidence from a huge suite, not confidence from luck, but confidence you can defend case by case, because you chose each one on purpose. Intentional selection is the whole game; combining techniques is how you play it on a real feature.
Check your understanding
Section titled “Check your understanding”- The decision guide maps input shapes to techniques. For each of these inputs, name the shape and the matching technique: an order total valid over
$0–$500; auser_role × action × resourcepermission check; a five-step checkout wizard; and eight independent feature-flag toggles. - In the shipping example, partitioning already picks a weight from the valid band. Why do we still need boundary value analysis on top of it — what does BVA catch that the partition representative cannot?
- The pairwise set and the decision table both covered the “international + express + non-member = reject” case. Is that overlap wasteful? Explain what each technique uniquely guarantees about that case.
- You have one hour to test the shipping calculator. Which cases do you write first and why? State the risk principle you are applying.
- What does it mean to make coverage “explicit,” and why is a suite of ten passing tests clustered near a comfortable middle value less trustworthy than a suite of six deliberately layered ones?
Show answers
- Order total
$0–$500is an ordered range → partitioning + BVA.user_role × action × resourceis a combination of conditions → decision table. The five-step wizard is history-dependent behaviour → state-transition testing. Eight independent toggles are many independent parameters → pairwise/combinatorial. - Partitioning picks one representative (e.g.
12.5 kg) and assumes the whole valid band behaves identically — but that assumption breaks at the edges. BVA tests30.00versus30.01(and0.00versus0.01), catching the off-by-one and<-versus-<=mistakes at the band’s edges that a comfortable middle value can never reveal. - Not wasteful — it is defence in depth, but each technique earns its place differently. The decision table uniquely guarantees the three-way condition (international and express and non-member) is tested and that the resulting action (reject) is correct per the rule. Pairwise uniquely guarantees every pair of parameter values appears cheaply, but it does not guarantee any specific three-way interaction, nor verify the rule’s outcome — it may cover the case only by luck.
- First: the highest-risk boundaries (
30.00/30.01,0.00/0.01) because off-by-ones are the most common defect and live at the edge; then the high-risk combinations — decision-table rules that reject or change the price (revenue/trust bugs). The principle is risk = likelihood × cost: spend scarce time where a bug is both likely and expensive. - Making coverage explicit means annotating each test with the shape and technique it targets, so a reviewer can trace every case to a reason and see the suite is intentional, not accidental. Ten tests clustered at a comfortable middle value all pass regardless of how the logic bends — they aren’t aimed at where bugs live — so they yield green checkmarks without confidence. Six deliberately layered tests each target a specific failure mode (an edge, a rule, an interaction), so they buy defensible confidence for fewer cases.