A rule can be easy to understand one condition at a time and still be difficult to test correctly when several conditions interact.

Consider a refund policy. A refund depends on whether the order is within 30 days, whether the item is damaged, and whether it was marked final sale. Writing a few examples from memory can miss an important combination. Adding every possible combination can create noisy tests that repeat the same reasoning.

A decision table makes the combinations explicit before they become test cases. It lists the conditions that affect a decision, the meaningful combinations of those conditions, and the expected outcome for each combination.

This article shows how to build a small decision table, turn it into tests, and avoid both missing cases and unnecessary combinatorial growth.

Start with the decision, not the code

Suppose the refund policy is:

  • normal items may be refunded within 30 days;
  • damaged items may be refunded even after 30 days;
  • final-sale items cannot be refunded.

Three conditions affect one outcome:

within 30 days?
damaged?
final sale?
        |
        v
refund allowed?

The first useful step is not to inspect branches in the implementation. Write the business decision independently. That keeps the table focused on behavior the software must provide rather than on the current shape of the code.

Build the smallest complete table

A direct table for three Boolean conditions has eight possible combinations:

Within 30 days Damaged Final sale Refund?
yes no no yes
no no no no
yes yes no yes
no yes no yes
yes no yes no
no no yes no
yes yes yes no
no yes yes no

This full table is useful while discovering the rules because it exposes conflicts. For example, a damaged final-sale item matches both “damaged items may be refunded” and “final-sale items cannot be refunded.” The table forces the policy to answer which rule takes precedence.

Here, final sale wins.

That is already valuable. The table has found an ambiguity that a collection of happy-path examples could easily hide.

Collapse conditions that do not matter

The full truth table is not always the best final test set.

When Final sale = yes, neither age nor damage changes the outcome. Those rows can be represented as one rule using - to mean “does not affect this outcome”:

Rule Within 30 days Damaged Final sale Refund?
A - - yes no
B - yes no yes
C yes no no yes
D no no no no

The reduced table expresses four distinct reasons for behavior rather than eight mechanical combinations.

This reduction is safe only when the omitted condition genuinely cannot change the result for that rule. A dash is a claim about irrelevance, not a shortcut for a case nobody considered.

Turn each rule into one focused test

Each row can now become a test case:

A: final-sale item -> refund denied
B: damaged non-final-sale item -> refund allowed
C: recent normal item -> refund allowed
D: old normal item -> refund denied

A table-driven test might represent them as data:

cases = [
  {within30Days: true,  damaged: false, finalSale: true,  want: false},
  {within30Days: false, damaged: true,  finalSale: false, want: true},
  {within30Days: true,  damaged: false, finalSale: false, want: true},
  {within30Days: false, damaged: false, finalSale: false, want: false},
]

This is language-neutral pseudocode. In production code, use the testing conventions of the language and make case names describe the rule being exercised.

Notice that rule B chooses one concrete value for within30Days even though the table says age does not matter. Executable tests need concrete inputs. The decision table tells us that either age value should lead to the same policy outcome under that rule.

If the irrelevance of age is itself important to protect, test both values or add a property-oriented test. Do not assume one representative example proves all values are equivalent.

Distinguish decision conditions from input values

A common mistake is putting raw inputs directly into the table too early.

The policy condition is within 30 days, not a particular order age such as 17 days. The decision table should usually describe the condition at the level where the rule changes.

Boundary tests then verify how raw values map to that condition:

29 days -> within window
30 days -> depends on the stated inclusive/exclusive rule
31 days -> outside window

This separation prevents one table from trying to solve two different testing problems:

raw input -> condition classification -> business decision

Test classification boundaries where they occur. Use the decision table to test how classified conditions interact.

Use tables to expose missing policy, not invent it

If two rules appear to conflict, the test author should not silently choose an outcome just to complete the table.

Suppose the requirements say:

VIP customers receive free shipping.
Hazardous goods require a shipping fee.

What happens when a VIP customer orders hazardous goods? The requirements do not say.

A decision table makes the missing combination visible. That is a question for the product or domain owner, not an invitation for the test to invent precedence.

Once the intended outcome is decided, record it in the table and tests. The table then becomes a compact explanation of the policy as well as a test-design aid.

Avoid combinatorial explosion

With n independent Boolean conditions, a complete truth table contains 2^n combinations. Ten conditions already imply 1,024 rows.

That does not mean decision tables stop being useful. It means the decision is probably too broad for one flat table, or many conditions are irrelevant under particular rules.

First look for dominance. In the refund example, final sale determines the result regardless of two other conditions. Collapsing irrelevant conditions removes redundant rows.

Next look for separate decisions. If fraud screening and refund eligibility are independent policies that happen to run in the same function, give them separate tables and test their integration at the boundary between them.

Finally, consider other test-design techniques when the goal is broad interaction coverage rather than exact rule enumeration. Pairwise or property-based approaches can be useful for different problems. A decision table is strongest when a finite set of conditions determines a discrete outcome and the rules themselves are worth reviewing explicitly.

Keep the table aligned with the contract

A decision table becomes misleading if it merely mirrors implementation branches.

Suppose the code contains a temporary flag that selects an old algorithm during migration. If that flag is not part of the user-visible refund policy, adding it as a business condition makes the table describe implementation history rather than the contract.

Prefer conditions that a reviewer can connect to the decision being specified. Implementation-specific paths can have their own lower-level tests where necessary.

The same principle applies when requirements change. If final-sale damaged items later become refundable for safety recalls, update the decision table as part of the policy change. A stale table can preserve the wrong behavior just as effectively as a stale test.

Know when a simpler test is better

Decision tables add the most value when several conditions interact and precedence matters.

A function with one condition does not need a table:

if balance < 0 -> reject
otherwise -> accept

Two direct examples may communicate that rule more clearly.

Decision tables are also a poor fit for continuous numerical behavior where the main risk lies in boundaries, formulas, or numerical precision. Boundary analysis and property tests may express those concerns better.

Use a decision table when you can naturally ask:

Which combinations of conditions lead to which discrete outcomes?

If that question does not match the problem, choose a test structure that does.

Review the table before the implementation

One practical advantage of a decision table is that people can review the policy without reading code.

Before implementing a complicated rule, walk through the rows with someone who understands the domain. Check that every condition has a clear meaning, every meaningful combination has an outcome, precedence is explicit, and each - truly means the condition is irrelevant.

Then make the executable tests correspond to those rules.

This order improves the feedback loop:

requirements
    -> decision table
        -> resolve ambiguity
            -> tests
                -> implementation

The table does not replace tests. It improves the reasoning used to choose them.

Conclusion

Complex conditional logic is difficult to test when cases are chosen from memory. Decision tables make the reasoning visible by connecting conditions, meaningful combinations, and expected outcomes.

Start with the business decision, enumerate enough combinations to expose conflicts, then collapse conditions only when they genuinely cannot affect the result. Keep boundary classification separate from rule interaction, and treat ambiguous cells as missing requirements rather than opportunities to guess.

The practical payoff is a test suite with fewer accidental gaps and fewer redundant cases, backed by a model that reviewers can understand before they inspect the implementation.