ProductHow it worksPricingBlogDocsLoginFind Your First Bug
A shipping-cost decision table with twelve rule columns collapsing into eight, the don't-care cells shaded, alongside the same eight rules rendered as a parameterised test
TestingDecision Table TestingTest Design Techniques

What Is Decision Table Testing? 12 Rules to 8

Tom Piaggio
Tom PiaggioCo-Founder at Autonoma

Decision table testing lays out every combination of independent conditions as columns, one column per rule, with the expected outcome in the bottom row. A complete decision table has four parts: the condition stub (the conditions themselves, listed as row labels), the condition entries (the value each rule assigns to each condition), the action stub (the outcome or outcomes being decided), and the action entries (the value each rule produces). Below is a full one, built and collapsed by hand, verified against a working parameterised test.

Every reference on decision table testing shows the same thing: a two-condition toy example, a shipping discount eligible or not, a cart total above some line or not, four rules, solved in a sentence. Then you're asked to build one for a real rule set with three conditions instead of two, and the toy example gives you nothing to check your work against.

That gap matters if you're the one being asked to produce a decision table for an interview, a certification, a junior engineer's first assignment, or an auditor who wants a term defined precisely enough to quote back. It matters less if you're a QA lead deciding how decision tables fit into a test strategy, or a team asking whether an AI-generated suite tests anything real; those are different questions, answered elsewhere. This one is about the artifact: a full table, correctly collapsed, becoming a real test.

A decision table testing example: shipping costs

Here's the rule set, in plain language before it becomes a table. Packages are Standard up to 20kg and Overweight past that. A domestic Standard shipment costs $8.00, a domestic Overweight shipment costs $16.00. International rates run higher: $24.00 Standard, $50.00 Overweight. Membership tier then adjusts that base rate. Plus members get 50% off, but the discount only applies domestically, so a Plus member shipping internationally pays the same rate as anyone else. Premium members ship domestically for free and pay a flat $18.00 internationally no matter how heavy the package is. Free-tier members pay the base rate as written, with no adjustment.

Three conditions decide the outcome, and each has a fixed set of values: weight class (Standard, Overweight), destination (domestic, international), and membership tier (free, plus, premium). Multiply the value counts and you get the rule count before anything gets collapsed: 2 × 2 × 3 = 12. That's the condition stub, and the twelve columns it demands, one per combination, none skipped.

The action here is a single line, shipping cost charged, so the action stub is one row and the twelve rules each fill in one action entry beneath it. Here's the full table, uncollapsed, split across the two weight classes so twelve columns stay readable at one width:

The uncollapsed shipping-cost decision table, Standard half: rules R1 to R6.
R1R2R3R4R5R6
Weight classStdStdStdStdStdStd
DestinationDomDomDomIntlIntlIntl
Membership tierFreePlusPremFreePlusPrem
Shipping cost$8.00$4.00$0.00$24.00$24.00$18.00
The uncollapsed shipping-cost decision table, Overweight half: rules R7 to R12.
R7R8R9R10R11R12
Weight classOverOverOverOverOverOver
DestinationDomDomDomIntlIntlIntl
Membership tierFreePlusPremFreePlusPrem
Shipping cost$16.00$8.00$0.00$50.00$50.00$18.00

The split is presentational, nothing more: it is one table of twelve rules, and the two halves are the two values weight class can take. Twelve columns, no gaps. That completeness is the format's whole point: an empty cell would mean a rule nobody decided, which the table makes visible in a way prose never does. The diagram in the next section puts all twelve back in one frame.

Rule collapsing in a decision table: 12 rules to 8

Two different things justify erasing a column in a decision table. An impossible combination is one the conditions themselves rule out: two values that can never both be true of the same record, a subscription that is simultaneously "trial" and "cancelled at signup," for instance. A don't-care condition is different. The combination is entirely possible; the action just doesn't depend on it in that region of the table. This shipping table has no impossible combinations. Weight, destination, and tier are independent facts about every order, so every collapse below is a don't-care collapse, not an elimination. Both the four-part structure and the don't-care convention predate software testing entirely, inherited from the same decision table notation used in operations research and business rule specification.

All twelve rules of the shipping-cost decision table in one grid, with the four collapsing pairs highlighted one at a time and joined by an arc naming the reduced rule they merge into, R3 with R9 into C5, R6 with R12 into C8, R4 with R5 into C6 and R10 with R11 into C7, finishing with all four pairs marked and a row showing which reduced rule every original rule maps to
Two different collapse reasons in one table. Solid outlines mark the pairs where weight is a genuine don't-care; dashed outlines mark the narrower case where only the Free and Plus tier values happen to produce the same rate. The bottom row is the receipt: every one of the twelve rules still maps to a reduced rule.

Rules 3 and 9 both charge $0.00: a Premium member shipping domestically pays nothing whether the package is Standard or Overweight. Weight is a don't-care condition in that cell. The same thing happens to rules 6 and 12: a Premium member shipping internationally pays a flat $18.00 no matter how heavy the package is. Rules 4 and 5 both charge $24.00, and rules 10 and 11 both charge $50.00, for a narrower reason: Plus's discount only applies domestically, so internationally a Plus member's rate happens to match a Free member's rate. Tier still matters in that region, a Premium member pays $18.00 there, so the merge only covers the Free and Plus values rather than turning tier into a full don't-care the way weight is in the other two pairs.

Four pairs collapse. Four rules stay exactly as written, the domestic Free and Plus rules for both weight classes, where weight and tier both genuinely change the price. Twelve rules become eight. Here's the reduced table below. "Any" marks a condition with no influence at all in that rule, weight genuinely cannot change the outcome in C5 and C8, while "Free/Plus" marks two values that happen to produce the same action while a third value, Premium, does not:

The reduced shipping-cost decision table: 12 rules collapsed to 8 by removing don't-care conditions. This table alone is a complete, correct specification.
C1C2C3C4C5C6C7C8
Weight classStdStdOverOverAnyStdOverAny
DestinationDomDomDomDomDomIntlIntlIntl
Membership tierFreePlusFreePlusPremFree/PlusFree/PlusPrem
Shipping cost$8.00$4.00$16.00$8.00$0.00$24.00$50.00$18.00

Check it against the full table above and every one of the original twelve rules is still accounted for: C1 through C4 are rules 1, 2, 7, and 8, untouched. C5 absorbs rules 3 and 9. C6 absorbs rules 4 and 5. C7 absorbs rules 10 and 11. C8 absorbs rules 6 and 12. Nothing was dropped, only merged where the action didn't care.

That reduced table is also the cleanest place to name the four parts of the format, because all four are visible in it at once and each one has a job. The condition stub lists the conditions once. The condition entries fill in what each rule assigns to them. The action stub names what's being decided. The action entries hold the answer, which is the value a test case eventually asserts against:

The reduced eight-rule shipping-cost decision table with its four parts numbered and highlighted in turn, the condition stub down the left, the condition entries across the eight rule columns, the action stub naming shipping cost, and the action entries holding each rule's expected dollar amount, finishing with rule column C4 highlighted top to bottom as one complete rule
The vocabulary maps onto four regions of one grid. The last highlight is the reading direction that trips people up: a rule is a column, so C4 read top to bottom is Overweight, domestic, Plus, $8.00.

Turning the decision table into a passing test

A decision table earns the name once every rule column becomes a test case, not a row in a document about test cases. Here's the function those eight rules describe, translated directly into the same branches:

'use strict';

// Condition 1: weight class. Standard is 20 kg or less, Overweight is above it.
const STANDARD_WEIGHT_LIMIT_KG = 20;

// Condition 2 and 3 collapse into these rate tables.
const DOMESTIC_BASE_RATE = { standard: 8, overweight: 16 };
const INTERNATIONAL_BASE_RATE = { standard: 24, overweight: 50 };

// A premium member ships domestically for free and pays a flat rate abroad,
// which is why weight is a dont-care condition on those two rules.
const PREMIUM_DOMESTIC_RATE = 0;
const PREMIUM_INTERNATIONAL_FLAT_RATE = 18;

const DESTINATIONS = ['domestic', 'international'];
const TIERS = ['free', 'plus', 'premium'];

function weightClassOf(weightKg) {
  return weightKg <= STANDARD_WEIGHT_LIMIT_KG ? 'standard' : 'overweight';
}

/**
 * Shipping cost for one parcel, derived from a three-condition decision table.
 *
 * @param {object} order
 * @param {number} order.weightKg      Parcel weight in kilograms.
 * @param {'domestic'|'international'} order.destination
 * @param {'free'|'plus'|'premium'} order.tier  Membership tier of the sender.
 * @returns {number} Cost in whole currency units.
 */
function calculateShippingCost({ weightKg, destination, tier }) {
  if (typeof weightKg !== 'number' || !Number.isFinite(weightKg)) {
    throw new TypeError('weightKg must be a finite number');
  }
  if (weightKg <= 0) {
    throw new RangeError('weightKg must be greater than 0');
  }
  if (!DESTINATIONS.includes(destination)) {
    throw new RangeError(`destination must be one of: ${DESTINATIONS.join(', ')}`);
  }
  if (!TIERS.includes(tier)) {
    throw new RangeError(`tier must be one of: ${TIERS.join(', ')}`);
  }

  const weightClass = weightClassOf(weightKg);

  if (destination === 'domestic') {
    if (tier === 'premium') {
      return PREMIUM_DOMESTIC_RATE;
    }
    const base = DOMESTIC_BASE_RATE[weightClass];
    return tier === 'plus' ? base / 2 : base;
  }

  // International. The plus discount is domestic only, so plus pays the free rate.
  if (tier === 'premium') {
    return PREMIUM_INTERNATIONAL_FLAT_RATE;
  }
  return INTERNATIONAL_BASE_RATE[weightClass];
}

module.exports = { calculateShippingCost, STANDARD_WEIGHT_LIMIT_KG };

And here's the reduced table again, this time as a parameterised test, one case per rule, checked against that function:

'use strict';

const { calculateShippingCost } = require('./shippingCost');

// One case per surviving rule of the reduced decision table. The full table is
// 2 weight classes x 2 destinations x 3 tiers = 12 rules. Four pairs of those
// rules merge, because in each pair one condition cannot change the outcome, so
// 8 rules survive.
//
// Weight is the dont-care condition where a premium member pays the same at any
// parcel weight: rules 3 and 9 merge into C5, rules 6 and 12 merge into C8.
//
// Tier is the dont-care condition on international parcels, where the
// domestic-only plus discount does not apply and a plus member pays the free
// rate: rules 4 and 5 merge into C6, rules 10 and 11 merge into C7.
//
// C1 through C4 are the four rules that carry over untouched (rules 1, 2, 7, 8).
const rules = [
  ['C1', 10, 'domestic', 'free', 8],
  ['C2', 10, 'domestic', 'plus', 4],
  ['C3', 25, 'domestic', 'free', 16],
  ['C4', 25, 'domestic', 'plus', 8],
  ['C5', 30, 'domestic', 'premium', 0],
  ['C6', 10, 'international', 'free', 24],
  ['C7', 25, 'international', 'free', 50],
  ['C8', 15, 'international', 'premium', 18],
];

describe('calculateShippingCost decision table', () => {
  test.each(rules)(
    '%s: %ikg %s %s costs %i',
    (rule, weightKg, destination, tier, expected) => {
      expect(calculateShippingCost({ weightKg, destination, tier })).toBe(expected);
    }
  );
});

Trace two of those cases by hand before trusting the table. C4, Overweight domestic Plus: the function sees a weight over 20kg, so it starts from the $16.00 Overweight base, then a Plus tier halves it, giving $8.00, matching the table exactly. C8, any weight international Premium: destination is international and tier is premium, so the function returns a flat $18.00 without ever inspecting the weight argument, which is the code's own version of the don't-care condition C8 already encodes; feed it 3kg or 30kg and the expected value doesn't move.

Decision table rule column C4 mapped field by field onto a single parameterized test case: weightKg 25, destination domestic, tier plus, expected 8
The rule column and the test case are the same four facts in two notations. The action entry becomes the expected value the assertion checks.

What makes each of those eight assertions meaningful, not just present, is a separate question; what makes a test assertion actually check something covers it.

How Autonoma turns business rules into tests

The gap every reference on decision table testing skips is the one that matters most: a table like this lives in a wiki page or a ticket, disconnected from the code it's supposed to describe, and nobody notices the drift until a support ticket does. The shipping function above, and the eight-row test that checks it, only stay honest as long as a person remembers to update both whenever the pricing rules change.

We built Autonoma's agents to close that gap by reading the codebase directly rather than working from a table someone wrote down separately. Our Planner agent reads a function like calculateShippingCost above, along with the components and flows that call it, and derives the conditions branching through it the same way the table above was built by hand: a weight check, a destination check, a tier check, each with its own branch. From there, our agents verify the resulting behavior by driving the actual running application in a live preview environment rather than asserting against the function in isolation, so a stated business rule becomes something checked end to end, on the checkout page a customer sees, not just a document describing what should happen.

That's a different layer than the parameterised unit test above, and it's worth being precise about the boundary. Pure arithmetic inside a single function, weight in, dollar amount out, is exactly the kind of logic a fast unit test already covers well, and Autonoma doesn't replace that layer or the person who wrote it. What our agents add sits above it: confirming the rule actually reaches the customer correctly once it's wired into a cart, a checkout flow, and whatever service charges the card, which a unit test checking the function in isolation can't see on its own.

Why decision table testing aged well

Most classical test design techniques exist because running a test case used to cost a person's time at a keyboard, so the job was picking a small, defensible sample out of a much larger space. A decision table was never that. Even in its original, uncollapsed form above, it isn't a sample of twelve rules chosen out of some larger set; it's the complete set, every legal combination, with the correct answer already sitting in the bottom row. Collapsing it isn't sampling either. It doesn't throw away a rule to save time running it. It merges rules that were always going to produce the identical answer, a statement about the specification, not a shortcut around executing it.

That's why a decision table survived the move to cheap test execution completely intact, while techniques built purely to reduce a case count changed job once the constraint they were built for disappeared. Pairwise testing exists to avoid running every combination, and equivalence partitioning collapses similar inputs before a table is even drawn; both had a case-reduction job to lose once execution stopped being scarce. A decision table never skipped a combination in the first place, so it had no such job to lose.

It also explains why this format, out of the seven named in the test design techniques hub, hands over cleanest to something reading business rules out of a codebase. There's no sampling decision to second-guess and no judgment call about which representative value to pick. Whatever your team's rules actually decide, weight and destination and tier or whatever else your product cares about, the table already states the answer for every legal input. Writing one down was never really about reducing how many cases you'd run. It was, and still is, about stating the rules somewhere unambiguous enough that a person, or a generator, can't get the mapping wrong. Where a table like this fits inside a broader test plan is a separate layer again, and our guide to organizing a test plan covers that in full.

If you came here needing a definition sharp enough to quote in an interview, a study guide, or an auditor's follow-up question, not a testing strategy document and not a verdict on whether an AI-generated suite is real, you have it: the four parts, a full table, the collapse, and the same table proven out as a passing test. If your actual problem is that your team's business rules live in three places, a table nobody trusts, a spec doc nobody updates, and a codebase that's quietly drifted from both, that's a bigger job than one blog post, and it's the specific gap Autonoma is built to close: reading the rules directly out of the code that enforces them, and keeping the tests that check them lined up with what the code says today, not what the table said six months ago.

Frequently Asked Questions

Decision table testing is a test design technique that lists every combination of independent conditions as a column, with the required outcome in the bottom row. A complete table has four parts: the condition stub listing the conditions, the condition entries showing which value each rule assigns to each condition, the action stub listing the outcomes being decided, and the action entries showing the value each rule produces. Because every legal combination gets its own column, a missing rule shows up as an empty or contradictory cell instead of staying silently unwritten.

Four components. The condition stub is the list of conditions down the left side, such as weight class, destination, and membership tier. The condition entries are the specific value each rule assigns to each condition, such as Standard, domestic, and free. The action stub lists the outcomes being decided, such as shipping cost charged. The action entries are the value each rule produces for that outcome, such as $8.00. Read top to bottom in a single column and you get one complete rule; read left to right across a row and you get every value one condition or action takes across the whole table.

Use a decision table in software testing when an outcome depends on the combination of several independent conditions rather than on any single input in isolation, which is common in pricing rules, discount eligibility, access or approval logic, and shipping or tax calculations. If the system only cares about one input at a time, equivalence partitioning or boundary value analysis is usually a better fit. If the system's behavior depends on what already happened rather than on a fixed set of conditions, a state transition model fits better than a table.

Rule collapsing is merging two or more columns of a decision table into one when they differ only in a condition that doesn't change the outcome, called a don't-care condition. It's distinct from removing an impossible combination, where the conditions themselves can never co-occur. A collapsed table should still cover every rule from the full, uncollapsed table; a legitimate collapse only removes a condition's influence in that specific region, it never removes a rule's outcome.

A decision table lists every combination of conditions flat, as columns, which makes it easy to spot a missing or contradictory rule at a glance but gets wide fast as conditions multiply. A decision tree branches sequentially, letting shared logic sit on one shared branch instead of being repeated across many columns, which reads more naturally when conditions depend on each other in sequence. Tables suit a fixed, independent set of conditions like the shipping rules above; trees suit conditions that are naturally hierarchical or evaluated in a specific order.

A unit test proves the pricing function returns the right number for a given set of conditions. It cannot prove the checkout page passes that function the right conditions in the first place, or that the amount it returns is the amount the customer is actually charged. Testing a pricing rule end to end means driving the real flow, adding the item, choosing the destination, signing in as the relevant membership tier, and asserting the total the customer sees, which is one row of the decision table per run. That is why a decision table hands over so cleanly to end-to-end testing: every rule column already states the expected result, so each column is a finished test case waiting for something to execute it.

Yes, though not by reading the table document, and the difference is the useful part. Autonoma reads the codebase the table is supposed to describe, finds the branching logic a pricing or eligibility rule actually runs through, and derives end-to-end cases from that, then verifies them by driving the running application in a live preview environment rather than asserting against the function in isolation. That helps most in the situation this article ends on: a table in a wiki, a spec doc nobody updated, and code that has quietly moved on from both. Tests derived from the code follow whichever of the three is actually charging your customers.

No, and it is worth being precise about the boundary. Weight in, dollar amount out is pure arithmetic, and a fast unit test covers it better than anything driving a browser can: it runs in milliseconds, it fails on the exact rule column that broke, and it needs no environment. Autonoma sits above that layer, confirming the rule still reaches the customer correctly once it is wired into a cart, a checkout flow, and whatever service charges the card. The right setup usually has both: the unit test proves the eight rules are implemented, and the end-to-end test proves the eight rules are reachable.

Related articles

Quara sits at the centre of a vast dark field of thousands of identical configuration tiles, with a single small lime-lit stack of tiles in front, illustrating pairwise testing compressing a full combinatorial matrix into a verified minimal set

Pairwise Testing Cuts 4,096 Configs to 22 Rows

Pairwise testing cuts 4,096 configurations to a verified 22 rows covering all 240 pairs. Here is the decision rule for when that reduction is actually worth it.

The seven test design techniques in software testing split into two groups: sampling rules that changed job once execution got cheap, and specifications that stayed the same

The 7 Test Design Techniques in Software Testing

All seven test design techniques in software testing, defined with one worked example each, plus a verdict on which ones survived cheap test execution.

A number line for a 1 to 90 day booking window folding at day 90, the exact point where an off-by-one boundary condition breaks

How Boundary Value Analysis Finds a 90-Day Off-by-One

Boundary value analysis taught on a booking-window off-by-one bug you can run: two-value, three-value, and robust BVA, verified with a parameterised test.

A horizontal agent trajectory diagram showing a tool call passing a right-tool checkpoint but failing an argument-accuracy checkpoint

How to Test AI Agents That Take Actions (Tool Calls)

A runnable guide to testing tool-calling agents: right tool, right order, right arguments, mocked vs live calls, failure handling, and non-determinism.