Table-Driven Tests in JavaScript

An alternate approach to writing and maintaining unit tests

Fuk Kwang
Technology @ Funding Societies | Modalku
5 min readJun 9, 2020

--

Original illustration by Sagar

Background

At Funding Societies | Modalku, we have made a commitment as a team to improve the test coverage of our backend micro-services and frontend web applications. In addition, for newer services that we write and newer features that we add, most pull-request reviewers specifically check for the availability and assess the quality of unit tests added.

Personally, I am an advocate of having good test coverage because of the benefits it brings. It allows me to refactor confidently and also presents an opportunity for me to share known edge-cases affecting the behaviour of a function that I write with my team-mates through code. Therefore, I have seen myself writing a lot of unit tests for over a year now specifically in Go and JavaScript.

I believe that a unit test-suite should:

  • Cover all the known code branches that the function execution can take
  • Serve implicitly as a documentation of the function’s behaviour with clear labels
  • List all the possible edge-cases with respect to the input data or state that the developer can think of

However, writing unit tests in JavaScript can get exhausting sometimes. Let us take a simple example of an add() function:

As you can see, although a library like Chai helps us write assertions in an idiomatic way, there is still a lot of duplication of code between the tests. If we want to test more input combinations, we need to add new test cases with similar assertion code. This makes the process understandably tedious for more complex functions with more branches or dependencies.

Table-driven tests

It was in the official Go wiki that I first came to know about table-driven tests. It is a fairly simple idea that encourages us to separate test case data from the imperative logic of test case assertions. We write the assertion logic once for a function and make it suitable to be run on a table of test-cases of any size as long as they conform to a certain shape.

It is a fairly simple idea that encourages us to separate test case data from the imperative logic of test case assertions.

Now let us try to re-implement our previous test suite with this idea in mind:

A few things to highlight here:

  1. We have declared a test as a JavaScript object that conforms to a custom schema. It includes the input parameters and the expected output or error.
  2. We have defined our suite as a set of these test objects.
  3. We have written a custom function that generates individual test cases using the language of the testing framework (in this case, mocha & chai)
  4. Finally, we pass the test object set to the custom function.

Since I have written many test suites that look like the one above, I have come up with a mini mental model to approach adding unit tests.

The shape of my test roughly looks like this: (represented as a TypeScript interface for readability)

The fields highlighted are values to be asserted and the un-highlighted fields are data attributes that depict either input or state.

Similarly, for the test suite itself, I have found the following template to work:

Here is an example of how this framework can be applied to test a more complex function that is more representative of the code that we regularly write unit tests for here at the company.

Note that this template or test schema is by no means complete. For instance, you may want to assert the number of times a dependent function was called in your test in which case you will have to extend it. I have only shared this here so that you can consider using it as a reference to ensure that your test tables are not wildly different from each other — between different functions and projects. Remember that you are essentially defining a custom DSL for each of your test suites. You must remember to keep it as consistent as possible to keep the cognitive overhead on your colleagues low when they review your tests or need to extend them later.

Summary

Overall, I have found that this approach has made my experience of writing unit tests in JavaScript a lot more pleasant. It is a simple idea but is quite potent and has the following benefits:

  • Decouples the declarative part of a test (data) from the imperative part (assertions) enabling us to, as the Go team puts it, ‘amortise the written code over all the entries in the table’ [sic].
  • Makes adding new tests arguably easier.
  • Improves test readability & maintainability by reducing code duplication.

--

--