Unit tests should form part of your documentation

Introduction

As developers, we generally fall short on documentation. We know we should do it, but when deadlines loom they're often the first thing to go. Even when we do manage to document our system fully, subsequent updates are often not added in full.

What does this mean? How will someone revisit our code 6 months down the road and make changes with confidence when our documentation is not comprehensive?

Self documenting code as a principle

We shouldn't forget that well written code is in a way self documenting. Avoiding naming variables p and q, instead using semantic names such as isAuthenticated and usersDateOfBirth are a good start. Continuing that naming convention with functions, classes, and file names will only take this further. We can make use of comments too, but these should be used sparingly, as semantically written code should be self explanatory for the most part. Comments provide value in places where we have complex calculations, or where the reason an approach has been taken is not obvious.

Bad:

// This function returns the number of users
const p = () => {  
    ...
}

Bad:

// This function returns the number of users
const getNumberOfUsers = () => {  
    ...
}

Good:

const getNumberOfUsers = () => {  
    ...
}

Also, we shouldn't overlook the importance of following standard patterns and favouring widely used libraries and tooling, as these will be well documented themselves and generally well understood.

Enter unit tests

There are still weapons at our disposal to make our code that much easier to understand. One of these that is often underplayed is unit tests. Sure, the primary job of unit tests may well be to prove the correctness of our code, but when written well they can provide one of the most useful forms of documentation of all.

Consider the case where you have a function performing a complex calculation based on some input. A comment here would not go amiss, but a comment may not provide enough detail by itself, and comments are often not updated. A more useful way to document this code would be to have several test cases that together demonstrate in an effective manner the behaviour of this function. Assuming that our unit tests form part of our deployment pipeline and that failing tests fail this pipeline, then our unit tests now become one of the few forms of documentation that we can fully trust to be up to date.

Features of unit tests that document code well

Almost any unit test will go some way towards helping document our code, but by following certain conventions we can make them go further.

While there isn't a right or wrong convention for writing unit tests, by following consistent patterns and conventions we improve the value of the unit tests as documentation. This is in much the same way as we would within our code.

A convention that I like to follow is using variable names within my tests that are agnostic of the semantics of the code under test. This involves assigning the function under test to target, result of the operation to result, and expected result to expectedResult. I also like to structure my tests using a given when then style. I supply my suite and assertions with descriptive names to make the test output more readable. An example is given below:

test('that some function', t => {  
    // (This is the given)
    const target = require('../src/some-functions').someFunction;

    // (This is the when)
    const input = 10;

    // (This is the then)
    const expectedResult = 150;
    const result = target(input);

    t.equals(result, expectedResult, 'calculates the correct value when given a valid input');
});

tl;dr

To make your code easy to understand, in addition to documentation you should write your code semantically, follow standard patterns, use widely used libraries and tooling and write semantic unit tests.