Decision Tables
The previous two techniques both looked at one input at a time. Equivalence partitioning split a single field into classes; boundary value analysis hammered the edges of one range. Both quietly assume the inputs are independent — that the correct behaviour for field A does not depend on the value of field B.
Real business rules break that assumption constantly. “Give the discount if the customer is a member and the cart is over $50 or it’s a holiday weekend.” Now the answer depends on a combination of conditions, and no amount of testing one input at a time is guaranteed to hit the specific mix that hides a bug. Decision tables are the technique for exactly this shape of problem: they take a tangle of if / and / or logic and lay every combination out as a grid, so the one combination someone forgot becomes impossible to miss. This is the third tool in the test-design toolkit, and the first one built for combinations rather than single values.
Model logic as conditions mapping to actions
Section titled “Model logic as conditions mapping to actions”A decision table has a rigid four-quadrant shape. The rows are split into conditions (the inputs your logic checks) on top and actions (the outputs or behaviours) on the bottom. The columns are split into a stub (naming each row) on the left and one column per rule on the right.
│ R1 R2 R3 R4 ← each column is ONE rule ──────────────┼──────────────────────── CONDITIONS │ is member? │ T T F F cart > $50? │ T F T F ──────────────┼──────────────────────── ACTIONS │ apply 10%? │ X - - - apply 0%? │ - X X XRead a single column top to bottom and it is a complete sentence: “If the customer is a member and the cart is over $50, then apply 10%.” That is the whole idea. Each rule column is one distinct path through the logic, expressed as a specific setting of every condition and the actions that must follow. Conditions are the inputs; actions are the outputs; a rule is the mapping between one full setting of the inputs and its outputs.
Because every condition appears on every rule, there is nowhere for a combination to hide. Informal reasoning (“well, obviously members always get the discount…”) skips cases silently. A table cannot: the column is either there or it is a hole you can point at.
Conditions do not have to be pure true/false. A condition can be multi-valued — “shipping region” might be one of {domestic, EU, rest-of-world}. The counting rule generalises cleanly: a table’s total rules are the product of each condition’s number of values. Three booleans give 2 × 2 × 2 = 8; two booleans and one three-valued condition give 2 × 2 × 3 = 12. The same completeness guarantee holds — you are simply walking a base-mixed counter instead of a binary one. For clarity this page keeps the examples boolean, but keep the product rule in mind the moment a condition has more than two outcomes.
Enumerate every combination
Section titled “Enumerate every combination”The power of the technique comes from a fact about counting. If you have N independent boolean (true/false) conditions, there are exactly 2^N distinct combinations of them — and therefore up to 2^N rules. Two conditions give 4 rules; three give 8; four give 16.
N conditions combinations (rules) ──────────── ──────────────────── 1 2 2 4 3 8 4 16 5 32Filling the table means walking the truth table in order, so no combination is left out. The mechanical way to lay out N conditions with no gaps and no duplicates: the top condition alternates every column, the next alternates in pairs, the next in fours — the standard binary counting pattern.
condition A │ T T T T F F F F condition B │ T T F F T T F F condition C │ T F T F T F T F R1 R2 R3 R4 R5 R6 R7 R8This is the whole reason the technique earns its place. Ask a developer to enumerate the cases from memory and they will list the four or five they thought of. The 2^N grid lists all of them, and the ones they didn’t think of — the weird corner where two features interact — are precisely where the bugs live.
The order of enumeration is not arbitrary, either. Because the pattern is mechanical, a second person — or a generator script — can reproduce your exact table and confirm you missed nothing. That reproducibility is what turns “we tested the combinations” from an assertion of faith into a claim anyone can audit column by column.
Worked example: a login policy
Section titled “Worked example: a login policy”Let’s build a real table end to end so the mechanics are concrete rather than abstract. Authentication is a perfect fit: the answer is never one input in isolation, it is always a conjunction of several checks that must all line up before access is granted.
A service decides whether to grant access based on three conditions:
- Valid password? — did the supplied password match?
- Account locked? — has the account been locked after too many failures?
- MFA passed? — did the second factor (one-time code) verify?
Three conditions → 2^3 = 8 rules. Enumerate them all:
│ R1 R2 R3 R4 R5 R6 R7 R8 ─────────────────┼─────────────────────────────── valid password? │ T T T T F F F F account locked? │ T T F F T T F F MFA passed? │ T F T F T F T F ─────────────────┼─────────────────────────────── grant access │ - - X - - - - - deny: locked │ X X - - X X - - deny: bad creds │ - - - - - - X X deny: MFA failed │ - - - X - - - -Only R3 grants access — everything must be right at once. Writing it as a grid immediately surfaces two questions informal reasoning glosses over:
- R1 and R2: the password is valid and MFA passed (R1) or failed (R2), but the account is locked. Does “locked” beat everything? The table forces the policy owner to decide — here, locked denies regardless of the other two. That is a precedence decision most spec prose leaves ambiguous.
- R4: valid password, not locked, but MFA failed. It is easy to write code that grants access the moment the password checks out and forgets the second factor. The table makes R4 a named, testable row you cannot skip.
Without the table, R2 and R4 are exactly the kind of combinations a hand-written test suite forgets — and each is a security hole.
That is the pattern to internalise: the table did not just tell us what to test, it forced a design conversation the prose spec left open. Building the grid is a specification activity as much as a testing one.
Collapse the table without losing coverage
Section titled “Collapse the table without losing coverage”Eight rules is manageable; 2^N grows fast, and 32 or 64 rules is a lot to test. Two moves shrink the table without dropping any coverage.
Remove infeasible combinations
Section titled “Remove infeasible combinations”Some combinations cannot physically occur. If a rule pairs “user is anonymous” with “user is an admin,” that column describes an impossible world — delete it. Be careful: only remove a rule you can prove is impossible from the domain, not one you merely think is unlikely. “Unlikely” combinations are where bugs hide; “impossible” ones waste a test.
Merge with don’t-care entries
Section titled “Merge with don’t-care entries”Look back at the login table. R1 and R2 both deny: locked, and they differ only in the MFA row — meaning, when the account is locked, MFA passing or failing makes no difference. That input is a don’t-care, written -, and the two columns collapse into one:
│ R1' R3 R4 R7' ─────────────────┼───────────────────── valid password? │ T T T F account locked? │ T F F - ← bad creds: locked-or-not doesn't matter MFA passed? │ - T F - ← locked: MFA doesn't matter ─────────────────┼───────────────────── grant access │ - X - - deny: locked │ X - - - deny: bad creds │ - - - X deny: MFA failed │ - - X -Eight rules became four. A - means “this condition does not affect the outcome for this rule,” so one collapsed column stands in for every setting of the don’t-care inputs — coverage is preserved, not thrown away. The discipline: enumerate all 2^N first, decide the action for each, then collapse. Collapsing before you enumerate is how you lose the combination you never noticed.
Two errors the full grid catches for free
Section titled “Two errors the full grid catches for free”Building the complete table is also a review of the specification itself, because two classes of spec defect become visible the moment every combination is on the page.
- Redundancy. Two rules with identical conditions that prescribe the same action are harmless duplication — but two rules with identical conditions prescribing different actions is a contradiction: the spec answers the same question two ways. In prose that conflict can sit undetected for years; in a table the two columns line up side by side and the disagreement is obvious.
- Incompleteness. If, after enumerating, some combination has no action marked, the specification never said what should happen in that case. That gap is a decision nobody made — and undefined behaviour is where crashes and security holes breed. The table turns “we forgot to specify this” into a visibly empty action column.
So a decision table pays off twice: once as a test plan, and once, earlier, as a specification review that finds contradictions and gaps before a single line of code is written.
Each surviving rule is one test case
Section titled “Each surviving rule is one test case”Here is the payoff that ties this technique back to the whole book. Every column that survives collapsing becomes exactly one test case. The condition rows tell you how to set up the test (the inputs); the action rows tell you what to assert (the expected output). There is no guesswork about what to test or how many — the table hands you both.
Turning the collapsed login table into executable tests is mechanical:
def authorize(valid_pw, locked, mfa_ok): if locked: return "deny: locked" if not valid_pw: return "deny: bad creds" if not mfa_ok: return "deny: MFA failed" return "grant access"
# One test per surviving rule — the table IS the test plan.assert authorize(True, True, True) == "deny: locked" # R1'assert authorize(True, False, True) == "grant access" # R3assert authorize(True, False, False) == "deny: MFA failed" # R4assert authorize(False, True, True) == "deny: bad creds" # R7' (locked=don't-care)Notice the guarantee this gives you: because the rules were enumerated from the full 2^N grid before collapsing, every combination of conditions is exercised by some test. A defect that only appears when “locked and valid password and MFA failed” all hold has a row — R1’ — and therefore a test. The table converts “did we cover everything?” from a hopeful shrug into a countable, checkable fact: rules covered ÷ rules total.
Under the hood — coverage this technique gives you
Section titled “Under the hood — coverage this technique gives you”A collapsed decision table where every rule has a test achieves decision-table coverage: every documented combination of conditions is exercised at least once. That is stronger than testing each condition independently (which would miss interactions) but it is not the same as the code-level metric MC/DC (modified condition/decision coverage) used in avionics, which additionally demands you show each condition independently flips the outcome. Decision tables work from the specification, so they can find a combination the code forgot to handle entirely — a bug no code-coverage metric can see, because there is no code to cover. The two are complementary: the table checks the spec is fully handled; MC/DC checks the boolean expression that implements it is fully exercised.
Best fit — and when it’s overkill
Section titled “Best fit — and when it’s overkill”Decision tables shine on complex business rules: eligibility checks, pricing and discount engines, insurance underwriting, permission and access policies, tax or benefit calculations — anywhere the correct answer depends on how several conditions combine. If a spec is full of “if… and… unless… except when…,” a decision table is almost always the right instrument.
They are overkill when inputs are simple and independent. A form with three unrelated fields — a name, an email, an age — has no combination logic; each field is validated on its own, so equivalence partitioning and boundary analysis already cover it, and a 2^N table just multiplies work for combinations that never interact. Reach for a decision table when the conditions talk to each other, and leave it on the shelf when they don’t.
There is also a middle ground worth naming. When the order of conditions matters — “the account can only be locked after three failed attempts, and only then does a reset apply” — you have crossed from a decision table into a sequence of states, and the right model is a state-transition diagram, the next technique in this Part. A decision table captures a snapshot: given these condition values right now, what happens? It says nothing about how you arrived at those values. If history matters, the table is the wrong lens, and that distinction — snapshot versus sequence — is the seam between this page and the next.
The architect’s lens
Section titled “The architect’s lens”- Why does it exist? Because single-input techniques assume inputs are independent, and real business rules aren’t — the correct output depends on how conditions combine. Decision tables model that combination explicitly instead of hoping a tester imagines the right mix.
- What problem does it solve? It guarantees completeness over combinations: enumerating 2^N rules surfaces every case, including the corner interactions informal reasoning silently skips, and hands you one test per rule.
- What are the trade-offs? Total combinatorial coverage in exchange for exponential growth — the 2^N count is unmanageable past five or six conditions, and you must invest effort collapsing infeasible and don’t-care rules to keep the table sane.
- When should I avoid it? When inputs are simple and independent with no combination logic — partitioning and boundary analysis already cover those. And when conditions are too numerous, sample with pairwise testing instead of enumerating all of them.
- What breaks if I remove it? Combination bugs ship. The one mix of conditions nobody wrote a test for — locked-account-with-valid-password, member-on-a-holiday-with-an-empty-cart — is exactly where the defect hides, and without a table there is no systematic force making it a case you must consider.
Check your understanding
Section titled “Check your understanding”- A decision table has four quadrants. Name them and explain what a single rule column represents when you read it top to bottom.
- You have four boolean conditions. How many rules does the full table have, and what pattern do you use to lay them out with no gaps or duplicates?
- In the login example, why are rules R2 and R4 the ones a hand-written test suite is most likely to forget, and what does each represent?
- What is a “don’t-care” entry, and why must you enumerate the full 2^N grid before collapsing rather than after?
- Give one kind of system where a decision table is the right tool and one where it is overkill, and justify each in terms of how the inputs relate.
Show answers
- The four quadrants are conditions (inputs, top rows) and actions (outputs, bottom rows), split left-to-right into the stub (the names of each row) and the rules (one column each). Read a rule column top to bottom and it is one complete sentence: a specific setting of every condition and the actions that must follow — one full path through the logic.
- Four booleans give 2^4 = 16 rules. Lay them out with binary counting: the top condition alternates every column (T T T T… then F…), the next alternates in pairs, the next in fours, and so on — guaranteeing every combination appears exactly once.
- R2 is valid-password + MFA-passed but account locked — it is easy to assume a locked account only matters when credentials are bad, so the “locked overrides everything” precedence goes untested. R4 is valid-password + not-locked but MFA failed — code that grants access the moment the password checks out forgets the second factor. Both are combinations informal testing skips, and both are security holes.
- A don’t-care (
-) marks a condition whose value does not change the outcome for that rule, letting two or more columns with identical actions merge into one — preserving coverage while shrinking the table. You must enumerate all 2^N first so you consciously decide the action for every combination; collapsing before enumerating risks silently dropping a combination you never evaluated. - Right tool: a discount/pricing or access-control engine — outputs depend on how membership, cart size, holidays, etc. combine, so the interactions must be enumerated. Overkill: a form with independent fields (name, email, age) — each is validated alone with no combination logic, so partitioning and boundary analysis suffice and a 2^N table just multiplies non-interacting cases.