A Student's Guide to Software Engineering Tools & Techniques »

Writing Testable Javascript

Authors: Li Kai

JavaScript is a powerful language. However, its flexibility leads to multiple ways for people to go about doing the same thing. The end result is that multiple collaborators working on a single project can produce different code that does the same thing.

That is why there is a need to follow a standard way of writing JavaScript - it allows for more maintainable cleaner and more beautiful code.

Good JavaScript code should be testable and reusable.

Writing Testable JavaScript

Avoid Coupling With Selectors

When writing front end code we would encounter code that manipulates the document object model (DOM). Let's look at one such example.

var DIV_STATUS_MESSAGE_SELECTOR = '#statusMessagesToUser';

function populateStatusMessageDiv(message, status) {
    var statusMessageDivToUser = document.querySelector(DIV_STATUS_MESSAGE_SELECTOR);
    ...
}

In this piece of code, we see that the selector is intrinsically tied to the function. This means that the function is tightly tied with the selector and is not easily testable as the test code has to generate the same markup in the test suite for the function to hook up to. In order to prevent such tight coupling, it is advised to leave the selector as a parameter, or sometimes, simply pass in the element itself.

function populateStatusMessage(selector, message, status) {
    var statusMessageDivToUser = document.querySelector(selector);
    ...
}

Split Business Logic and Presentation Code

When we have to write code that generates markup, it requires business logic. However, mixing the two up is not a good idea. Let's look at the following example:

fetch(
  '/admin/adminStudentGoogleIdReset?' + params,
  { method: 'POST' }
)
  .then(data => {
    document.getElementById('result').innerHTML = data;
  })
  .catch(error => {
    document.getElementById('error').innerHTML =
        'An error occurred, please Retry';
  });

It order to test such a function, we would now have to incorporate both logic and also the mock-up generated. Splitting the logic and markup into two separate functions will both make it easier to test and composable because now you can reuse code that generates the markup in multiple places.

function processData(data) {
    document.getElementById('result').innerHTML = data;
}

function handleError(error) {
    document.getElementById('error').innerHTML =
      'An error occurred, please Retry';
}

fetch(
  '/admin/adminStudentGoogleIdReset?' + params,
  { method: 'POST' }
)
  .then(processData)
  .catch(handleError);

Already, we are seeing some of the patterns that lead to the MVC, albeit in a very small scale.

Avoid Big Anonymous Functions

Although anonymous functions can lead to cleaner and shorter code, critical business logic should not be written in anonymous functions. The lack of namespace makes them impossible to test. This is common, and tempting when the code starts off with a listener for the DOMContentLoaded event.

The testable way of writing such function is to simply give the function a name, which allows it to be tested.

document.addEventListener("DOMContentLoaded", function(event) { 
  bindStudentPhotoLink('.profile-pic-icon-view-link');
});

function bindStudentPhotoLink(selector) {
    ...
}

Of course, this may be a problem if two Javascript functions have the same name, as they are all in the global scope. This is where the module pattern (see: Namespacing in Javascript) can be used.

var myApp = (function() {

    var id= 0;

    return {
        next: function() {
            return id++;
        },

        reset: function() {
            id = 0;
        }
    };
})();

As demonstrated above, only myApp is declared in the global scope, and we can access its methods through the object notation, e.g. myApp.next(). This is also especially useful to declare private variables that be used among functions. id is not accessible outside of scope in this example.

Purity is Worth Pursuing

There are tonnes of literature about functional programming. I will heavily recommend reading the Mostly adequate guide to Functional Programming. It explains functional concepts extremely well.

The benefit of pure functions is simple, there is no need to keep track of state. Given an input, the output is guaranteed to be the same every time. This allows us to write extremely simple unit tests, instead of having to maintain the state while testing.

var counter = 0;
var todos = [];

function getTodo() {
    counter++;
    if (counter < todos.length) {
        return 'NIL';
    }
    return todos[counter];
}
...
getTodo();

The above function getTodo is not stateless as it depends on counter's value. In order to write the tests, we would need to ensure counter is reset to the same value at the end of each test. A better way would be to do this:

var counter = 0;
var todos = [];

function getTodo(counter, todos) {
    if (counter < todos.length) {
        return 'NIL';
    }
    return todos[counter + 1];
}
...
getTodo(counter, todos);

Now, not only that the person who writes the unit test can write in fewer lines of code, you can also use the function for some other state other than the global values.

Writing Reusable Javascript

Optional Parameters

When writing certain functions, there would be certain situations where we want to have optional parameters. The easiest way would be to put optional parameters at the end of the parameter calls, like such:

// es5 syntax
function createPopUp(title, content, status, headerColor, bodyColor) {
    var headerColor = headerColor || 'default';
    var bodyColor = bodyColor || 'default';
    ...
}

// es6 syntax default parameters
function createPopUp(title, content, status, headerColor = 'default', bodyColor = 'default') {
    ...
}

This is relatively simple and easy to understand. However, in order to specify body color, the user would have to know and fill in the default header color.

// es5 syntax
function createPopUp(title, content, status, optionals) {
    var headerColor = optionals.headerColor || 'default';
    var bodyColor = optionals.bodyColor || 'default';
    ...
}
// es6 syntax with destructuring and default parameters
function createPopUp(title, content, status, { headerColor = 'default', bodyColor = 'default' }) {
    ...
}

By using an object, the user would just need to fill in what parameter they want to be changed. In large functions, this would result in better readability as well.

createPopUp('Warning', 'This will delete everything!', dangerStatus, { bodyColor: 'red' });

Understand Method Chaining

Method chaining is syntax such as array.concat([1, 2]).filter(isEven). It is also sometimes referred to as the fluent interface. (see: Method Chaining in JavaScript)

It is achieved by returning the object itself in the call.

var obj = {
    save: function() {
        // some procedures
        return this;
    }
};

Method chaining can be found in other languages but it is very common in Javascript. It can be found in libraries such as JQuery and lodash. Compare the two snippets below:

var bob = new Person();

bob.setName('Bob');
bob.setAge(16);
bob.setGender('male');

bob.save();
new Person()
  .setName('Bob')
  .setAge(16)
  .setGender('male')
  .save();

Not only is the method chaining much shorter, it also makes the code more maintainable by keeping properties organised nicely and easily traceable in version control.

However, like any design pattern, this is not to be abused. Only use method chaining when methods are related to the object at hand.

Addendum

As with all guides, this list of good practices and advices are not exhausive. Good code takes practice and finese, and if you discover ways to make code better, feel free to contribute and add them to this document!

Resources

Clean Code Javascript Apparently most of what I wrote appears in this huge guide in some form. It's an amazing resource and also explains SOLID clearly near the bottom.

Airbnb Javascript Style It is one thing to follow the style guide, and another to understand why it is that way. Understanding why Airbnb chose certain constructs and syntax reveals ways to write code that is clean, understandable and maintainable.

JavaScript Design Patterns The title says it all. From the most common to obscure patterns, this book covers design patterns and explains trade offs. Although it specifically caters to Javascript, it's recommended reading for all prospective software engineers.

References

Namespacing in Javascript

Method Chaining in JavaScript