Use decision tables to write better tests faster

eddyy stop
6 min readOct 14, 2018
A decision table.

Decision tables are an excellent tool to use in both project and user acceptance testing. Decision tables make it easy to see that all possible combinations of conditions have been tested. Its easy to see when conditions are missed and its easy to add new conditions.

Two kings, one pawn

One of my friends is a Chess Grandmaster. He wrote a program for a Computer Science project which determined if a Chess position containing 2 kings and 1 pawn was a win or a draw for the side with the pawn.

These positions are completely solved in the Chess literature, so the program did not need to play Chess moves. It only needed to encapsulate the known results, though those are complex themselves.

My friend wrote an impressive number of nested if statements. He tested his program by having it run through every possible position, followed by printing test results like the simplistic one below. He carefully compared these charts with the Chess literature.

White to move wins if his pawn is on a square with a +

I mentioned to him that instead of writing his ~3,000 lines of code (LOC) program, he could have written one with ~30 LOC which scanned charts like his test output.

I remember a longish silence. My friend had just discovered the power of decision tables.

Simple cases

Decision trees allow you to look up information in a table rather than using logic statements such as if and case.

In simple cases, it’s quicker and easier to use logic statements. Compare these two Mocha tests.

// No decision table
it ('produces expected results', () => {
assert.equal(isInteger(-1, true, -1);
assert.equal(isInteger(0, true, 0);
assert.equal(isInteger(1, true, 1);
assert.equal(isInteger(1.1, false, 1.1);
assert.equal(isInteger('1'', false, 'string 1');
});
// Using a decision table
const decisionTable = [
[-1, true], [0, true], [1, true], [1.1, false], ['1', false]
];
it ('produces expected results', () => {
decisionTable.forEach(([ value, result ]) => {
assert.equal(isInteger(value, result, value);
});
});

Let’s rewrite a test to use decision tables

As the logic gets more complex , decision table-driven code is simpler than complicated logic, easier to modify and more efficient. We can investigate these claims by rewriting an existing test module into one which uses decision tables.

FeathersJS is a popular server framework which runs on Node.js and sits on top of Express. It’s hooks are pluggable middleware functions which are called either before or after a database call. I’ve written various hooks commonly used in Feathers — I’m a Feathers maintainer — including one called discard which removes fields from records.

Don’t worry if you’re not familiar with some of the above, or if you’re not familar with the Mocha test framework. You’ll still get the point easily enough.

The existing discard test does not use decision tables and is 201 LOC. The rewritten version using decision tables is just 65 LOC. That’s 66% fewer lines of code, which is huge. The reduction arises from the elimination of repetitive boilerplate; essentially each test is just 1 row in the decison table.

“That’s 66% fewer lines of code, which is huge.”

Each of the 15 original tests looks similar to this

One original test

The first 8 of those original tests have been replaced by

The first eight tests using a decision table

Frankly, we could have replaced the remaining 7 original tests by adding just 7 more lines to the decision table.

The pro’s of using decision tables

  • There is less “active” code. Only lines 28–39 are executed.
  • There is less code overall. The original 201 LOC have been reduced to 65.
  • It’s much easier to understand what is being tested. You can look at the decision table instead of reading 8 different pieces of code.
  • It’s easier to see what is not being tested. For example, these tests only consider context.params.provider set to undefined or ‘rest’. The remaining options of ‘socketio’ and ‘primus’ are never tested. That could be, and is, intentional based on a knowledge of the internals of Feathers, but nonetheless the decision table makes it obvious.
  • It’s easier to decide which other conditions should be tested.
  • Its easy to add new tests.

The con’s of using decision tables

  • The decision tables can get wide.
  • Its easier to understand a decision table if it’s columns are aligned vertically. That’s why the two eslint comments are included. They prevent your linter from reformatting the lines, removing multiple spaces.

It’s easy to add more tests. We’ve mentioned above that the valid values for the provider column were undefined, ‘rest’, ‘sockertio’, and ‘primus’. We could test these exhaustively by expanding the 8 tests above to 29.

Expanding the original 8 tests to 29

All it took was adding 21 lines, one line for each test.

Building decision tables

Decision tables are divided into 2 parts: the conditions and the results.

Decision tables consist of conditions and results

Add a condition column whenever a new input is needed for the tests. Add a result whenever a new output from the tests needs to be tested.

One test module may include several different decision tables, like in our rewritten module. Each table can be oriented toward different types of conditions and results.

Testing user permissions

Testing user permissions on database access is certainly one of the most useful tests for a database table’s service.

This module tests role based access control (RBAC) for the Feathers database service posts. One half of it’s decision table looks like this

48 of the 96 tests in the decision table

These 96 complicated async tests are run by the 180 JavaScript statements in the module, an average of less than 2 statements per test. This is a debatable statistic but, however you look at it, that’s efficient.

Partial console log from the permission test

“These 96 complicated async tests are run by the 180 JavaScript statements in the module … that’s efficient.”

Where else can you use decision tables?

Tests are not the only place you can use decision tables. In fact, you may be using them elsewhere already! Here are some perhaps unexpected examples of decision tables.

// Decision table constructed as an array
daysPerMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
// Decision table as a hash
adapterInfo = {
generic: { idName: '_id', idType: 'string' },
memory: { idName: '_id', idType: 'number' },
nedb: { idName: '_id', idType: 'string' },
mongodb: { idName: '_id', idType: 'string' },
mongoose: { idName: '_id', idType: 'string' },
sequelize: { idName: 'id', idType: 'number' },
knex: { idName: 'id', idType: 'number' },
rethinkdb: { idName: 'id', idType: 'number' },
};

As you can see, you use decision tables all the time; they are not an exotic construct. You may just want to look for more opportunities to use them, in more complex scenarios.

Code Complete by Steve McConnell, Chapter 18, talks about other uses for decision tables including:

  • Insurance Rates.
  • Intepreting flexible message formats.
  • Fudging lookup keys.
  • Indexed access tables.
  • Stair case access tables.
  • Memory paging.
  • Substituting table lookups for complex expressions.
  • Precomputing values.

“Tables provide an alternative to complicated logic and inheritance structures. If you find that you’re confused by a program’s logic or inheritance tree, ask yourself whether you could simplify by using a lookup table.” — Code Complete by Steve McConnell

In conclusion

You’ve seen how decision tables are an excellent tool to use in both project and user acceptance testing. Decision tables make it easy to see that all possible combinations of conditions have been tested. Its easy to see when conditions are missed, and its easy to add new conditions.

Subscribe to Feathers-plus publications to be informed of coming articles. Some of them, like this article, are of general interest and not specific to Feathers.

If you want to look into Feathers, feel free to join Feathers Slack to join the discussion or just lurk around.

--

--