Decision Table Testing — Handling Complex Business Rules Systematically

Some features are driven by simple input ranges that EP and BVA handle well. But many real-world features involve combinations of conditions — if the user is a premium member and the order is over $100 and the coupon is valid, then apply a 20% discount; otherwise apply 10%; unless the item is already on sale, in which case no additional discount applies. Decision table testing is the technique designed for exactly this kind of multi-condition business logic, ensuring every combination is tested and no rule is missed.

Decision Tables — Systematic Combinatorial Testing

A decision table lists all possible combinations of conditions (inputs) and the corresponding actions (outputs) in a grid format. Each column is a rule — a unique combination of conditions that produces a specific action. Each rule becomes a test case.

# Decision Table: Free shipping eligibility
# Conditions:
#   C1: Order total >= $50?
#   C2: Customer is a loyalty member?
#   C3: Shipping destination is domestic?
# Actions:
#   A1: Free standard shipping
#   A2: Discounted shipping ($3)
#   A3: Standard shipping rate ($8)

# Build the decision table
conditions = ["Order >= $50", "Loyalty member", "Domestic shipping"]
actions = ["Free shipping", "Discounted ($3)", "Standard ($8)"]

# Each rule is a combination of condition values → action
rules = [
    # Rule 1: All conditions true
    {"C1": True,  "C2": True,  "C3": True,  "action": "Free shipping",
     "desc": "Big order + loyalty + domestic = free shipping"},
    # Rule 2: Big order + loyalty + international
    {"C1": True,  "C2": True,  "C3": False, "action": "Discounted ($3)",
     "desc": "Big order + loyalty + international = discounted"},
    # Rule 3: Big order + non-member + domestic
    {"C1": True,  "C2": False, "C3": True,  "action": "Free shipping",
     "desc": "Big order + domestic = free (even non-member)"},
    # Rule 4: Big order + non-member + international
    {"C1": True,  "C2": False, "C3": False, "action": "Standard ($8)",
     "desc": "Big order but international non-member = standard rate"},
    # Rule 5: Small order + loyalty + domestic
    {"C1": False, "C2": True,  "C3": True,  "action": "Discounted ($3)",
     "desc": "Loyalty member domestic = discounted regardless of amount"},
    # Rule 6: Small order + loyalty + international
    {"C1": False, "C2": True,  "C3": False, "action": "Standard ($8)",
     "desc": "Loyalty but international small order = standard rate"},
    # Rule 7: Small order + non-member + domestic
    {"C1": False, "C2": False, "C3": True,  "action": "Standard ($8)",
     "desc": "No qualifiers = standard rate"},
    # Rule 8: All conditions false
    {"C1": False, "C2": False, "C3": False, "action": "Standard ($8)",
     "desc": "Nothing qualifies = standard rate"},
]

# Display the decision table
print("Decision Table — Free Shipping Eligibility")
print("=" * 75)
header = f"{'Condition':<22}"
for i in range(len(rules)):
    header += f" R{i+1:>2}"
print(header)
print("-" * 75)

for c_name in conditions:
    row = f"{c_name:<22}"
    for rule in rules:
        key = {"Order >= $50": "C1", "Loyalty member": "C2",
               "Domestic shipping": "C3"}[c_name]
        row += f" {'Y':>3}" if rule[key] else f" {'N':>3}"
    print(row)

print("-" * 75)
row = f"{'→ Action':<22}"
for rule in rules:
    label = {"Free shipping": "Free", "Discounted ($3)": "$3", "Standard ($8)": "$8"}
    row += f" {label[rule['action']]:>3}"
print(row)

print(f"\n3 conditions × 2 values each = {2**3} rules = {len(rules)} test cases")
print(f"Each rule becomes one test case with specific test data.")
Note: The number of rules in a decision table is 2N where N is the number of binary conditions. Three conditions produce 8 rules; four conditions produce 16; five produce 32. When the number of conditions is large, the table becomes impractical. In those cases, use pairwise testing (covering all pairs of condition combinations rather than all full combinations) to reduce the number of test cases while still catching the majority of interaction defects.
Tip: Decision tables are excellent communication tools during requirements reviews. When you present a decision table to the product owner and ask “is this correct for Rule 4?” you often discover business rules that nobody had documented. The visual grid format makes it easy for non-technical stakeholders to spot errors: “Wait — a loyalty member should get discounted shipping internationally, not standard rate.” This collaborative validation catches requirements defects before any code is written.
Warning: Do not skip rules because they seem “impossible” or “unlikely.” A rule where all conditions are false (Rule 8 in our example) might seem obvious, but it is precisely the default case that developers sometimes forget to handle — the code has if/else branches for the special cases but no explicit handling for “none of the above.” Test every rule in the table, including the ones that seem trivial.

Common Mistakes

Mistake 1 — Only testing the “interesting” rules and skipping defaults

❌ Wrong: Testing Rules 1-3 (the free shipping and discounted cases) and skipping Rules 7-8 (standard rate defaults).

✅ Correct: Testing every rule because each represents a unique combination that the code must handle. Default cases are where “fall-through” bugs hide — the code handles special cases but crashes or produces wrong results for the ordinary path.

Mistake 2 — Not validating the decision table with the product owner

❌ Wrong: Building the decision table from the requirements document alone and assuming every rule is correct.

✅ Correct: Reviewing the completed decision table with the product owner to confirm that every rule’s action is the intended business behaviour. This review frequently uncovers undocumented rules and corrects misinterpretations.

🧠 Test Yourself

A feature has 4 independent binary conditions (yes/no). How many rules does the complete decision table contain?