Developing unit tests for JavaScript has become much simpler than before, regardless of using Node with frameworks like Mocha or Jasmine, or running tests in a headless browser environment like PhantomJS.
However, this doesn’t mean writing easily testable code is a walk in the park. Creating well-structured and easily testable code demands effort and planning. However, there are certain patterns, drawing inspiration from functional programming principles, that can be employed to prevent complications when it’s time to test our code. In this article, we will delve into some valuable techniques and patterns for writing testable JavaScript code.
Keep Business Logic and Display Logic Separate
One of the fundamental tasks of a browser-based JavaScript application is to listen for DOM events initiated by the user and then respond by executing specific business logic and presenting the results on the page. It’s tempting to write an anonymous function that handles most of the work right where you’re setting up your DOM event listeners. The issue with this approach is that it forces you to simulate DOM events in order to test your anonymous function, potentially creating overhead both in terms of code length and test execution time.
Instead, define a named function and pass it to the event handler. This allows you to directly write tests for named functions without the need to go through the trouble of triggering artificial DOM events.
This principle applies beyond the DOM. Many APIs, both in the browser and in Node, are structured around emitting and listening to events or waiting for other types of asynchronous operations to complete. A general guideline is that if you find yourself writing numerous anonymous callback functions, your code might not be easy to test.
| |
Employ Callbacks or Promises with Asynchronous Code
In the preceding code example, our modified fetchThings function executes an AJAX request, which performs most of its work asynchronously. This implies that we can’t simply run the function and check if it performed as expected because we wouldn’t know when it finishes running.
A common solution to this problem is to pass a callback function as an argument to the function that runs asynchronously. In your unit tests, you can then place your assertions within the callback you provide.

Another widely used and increasingly popular approach for structuring asynchronous code is with the Promise API. Conveniently, $.ajax and most of jQuery’s asynchronous functions already return a Promise object, covering many common scenarios.
| |
Minimize Side Effects
Strive to write functions that take arguments and return a value solely based on those arguments, similar to inputting numbers into a mathematical equation to obtain a result. If your function relies on some external state (e.g., properties of a class instance or the content of a file) and you need to set up that state before testing your function, it adds more setup overhead to your tests. You would have to assume that no other running code is modifying the same state.

Similarly, avoid writing functions that modify external state (such as writing to a file or saving values to a database) during their execution. This prevents side effects that could impact your ability to test other code reliably. In general, it’s best to keep side effects as close to the boundaries of your code as possible, with minimal “surface area.” In the case of classes and object instances, the side effects of a class method should be limited to the state of the class instance under test.
| |
Utilize Dependency Injection
A common pattern to reduce a function’s reliance on external state is dependency injection - providing all of a function’s external dependencies as function parameters.
| |
One of the main advantages of using dependency injection is that you can pass mock objects from your unit tests. These mock objects don’t cause actual side effects (in this example, updating database rows), allowing you to simply assert that your mock object was interacted with as expected.
Give Each Function a Single Responsibility
Decompose lengthy functions that perform multiple tasks into a series of concise, single-purpose functions. This makes it significantly easier to test that each function performs its specific role correctly, as opposed to hoping that a large function is doing everything right before returning a value.
In functional programming, the practice of chaining together multiple single-purpose functions is known as composition. Underscore.js even provides a function, _.compose, which takes a list of functions and chains them together. It passes the return value of each step as an argument to the subsequent function in the sequence.
| |
Avoid Parameter Mutation
In JavaScript, arrays and objects are passed by reference instead of value, and they are mutable. This means that when you pass an object or an array as an argument to a function, both your code and the function you passed the object or array to can modify the same instance of that array or object in memory. This implies that when testing your own code, you need to trust that none of the functions your code invokes are altering your objects. With every new location in your code that modifies the same object, it becomes increasingly challenging to keep track of the object’s expected state, making testing more difficult.

Instead, if you have a function that accepts an object or array, have it operate on that object or array as if it were read-only. Create a new object or array within the function and populate it with values based on your requirements. Alternatively, use Underscore or Lodash to create a copy of the passed object or array before manipulating it. Even better, employ a tool like Immutable.js, which creates read-only data structures.
| |
Write Tests Before Code
The practice of writing unit tests before the code they are intended to test is known as test driven development (TDD). Many developers find TDD to be highly beneficial.
By writing your tests first, you’re compelled to consider the API you’re exposing from the viewpoint of a developer using it. It also helps ensure that you’re only writing the necessary code to fulfill the contract enforced by your tests, rather than over-engineering an overly complex solution.
In reality, TDD is a discipline that can be challenging to adhere to for all code modifications. However, when it seems worthwhile, it serves as an excellent method to guarantee that all your code remains testable.
Conclusion
We are all aware of the common pitfalls encountered when developing and testing intricate JavaScript applications. However, by applying these tips and consistently aiming for code that is as straightforward and functional as possible, we can maintain high test coverage and minimize overall code complexity.