blog

  • Home
  • blog
  • Learning JavaScript Test-Driven Development by Example

Learning JavaScript Test-Driven Development by Example

You’re probably already familiar with automated testing and its benefits. Having a set of tests for your application allows you to make changes to your code with confidence, knowing that the tests have your back should you break anything. It’s possible to take things a step further and write your tests before you write the code; a practice known as Test-driven development (TDD).

In this tutorial, we will talk about what TDD is and what benefits it brings to you as a developer. We’ll use TDD to implement a form validator, which ensures that any values input by the user conform to a specified set of rules.

What is TDD?

Test-driven development is a programming methodology with which one can tackle the design, implementation, and testing of units of code, and to some extent the expected functionality of a program.

Complementing the test-first approach of Extreme Programming, in which developers write tests before implementing a feature or a unit, TDD also facilitates the refactoring of code; this is commonly referred to as the Red-Green-Refactor Cycle.

The red-green-refactor cycle associated with test-driven development

  • Write a failing test – write a test that invokes your logic and assert that the correct behavior is produced

    • In a unit test, this would be asserting the return value of a function or verifying that a mocked dependency was called as expected
    • In a functional test, this would be ensuring that a UI or an API behaves predictably across a number of actions
  • Make the test pass – implement the minimum amount of code that results in the test passing, and ensure that all other tests continue to pass

  • Refactor the implementation – update or rewrite the implementation, without breaking any public contracts, to improve its quality without breaking the new and existing tests

I’ve used TDD to some extent since I was introduced to it at the beginning of my career, but as I have progressed to working on applications and systems with more complex requirements, I have personally found the technique to be time-saving and conducive to the quality and robustness of my work.

Before proceeding, it might be worth familiarizing yourself with some of the various types of automated tests that can be written. Eric Elliot summarises them well:

  • Unit tests – ensure that individual units of the app, such as functions and classes, work as expected. Assertions test that said units return the expected output for any given inputs
  • Integration tests – ensure that unit collaborations work as expected. Assertions may test an API, UI, or interactions that may result in side-effects (such as database I/O, logging, etc…)
  • End-to-end tests – ensure that software works as expected from the user’s perspective and that every unit behaves correctly in the overall scope of the system. Assertions primarily test the user interface

Benefits of Test-Driven Development

Immediate test coverage

By writing test cases for a feature before its implementation, code coverage is immediately guaranteed, plus behavioral bugs can be caught earlier in the development lifecycle of a project. This, of course, necessitates tests that cover all behaviors, including error handling, but one should always practice TDD with this mindset.

Refactor with confidence

Referring to the red-green-refactor cycle above, any changes to an implementation can be verified by ensuring that the existing tests continue to pass. Writing tests that run as quickly as possible will shorten this feedback loop; while it’s important to cover all possible scenarios, and execution time can vary slightly between different computers, authoring lean and well-focused tests will save time in the long term.

Design by contract

Test-driven development allows developers to consider how an API will be consumed, and how easy it is to use, without having to worry about the implementation. Invoking a unit in a test case essentially mirrors a call site in production, so the external design can be modified before the implementation stage.

Avoid superfluous code

As long as one is frequently, or even automatically, running tests upon changing the associated implementation, satisfying existing tests reduces the likelihood of unnecessary additional code, arguably resulting in a codebase that’s easier to maintain and understand. Consequently, TDD helps one to follow the KISS (Keep it simple, stupid!) principle.

No dependence upon integration

When writing unit tests, if one is conforming to the required inputs, then units will behave as expected once integrated into the codebase. However, integration tests should also be written to ensure that the new code’s call site is being invoked correctly.

For example, let’s consider the function below, which determines if a user is an admin:

'use strict'

function isUserAdmin(id, users) {
    const user = users.find(u => u.id === id);
    return user.isAdmin;
}

Rather than hard code the users data, we expect it as a parameter. This allows us to pass a prepopulated array in our test:

const testUsers = [
    {
        id: 1,
        isAdmin: true
    },

    {
        id: 2,
        isAdmin: false
    }
];

const isAdmin = isUserAdmin(1, testUsers);
// TODO: assert isAdmin is true

This approach allows the unit to be implemented and tested in isolation from the rest of the system. Once there are users in our database, we can integrate the unit and write integration tests to verify that we are correctly passing the parameters to the unit.

Test-Driven Development With JavaScript

With the advent of full-stack software written in JavaScript, a plethora of testing libraries has emerged that allow for the testing of both client-side and server-side code; an example of such a library is Mocha, which we will be using in the exercise.

A good use case for TDD, in my opinion, is form validation; it is a somewhat complex task that typically follows these steps:

  1. Read the value from an <input> that should be validated
  2. Invoke a rule (e.g. alphabetical, numeric) against said value
  3. If it is invalid, provide a meaningful error to the user
  4. Repeat for the next validatable input

There is a CodePen for this exercise that contains some boilerplate test code, as well as an empty validateForm function. Please fork this before we start.

Our form validation API will take an instance of HTMLFormElement (<form>) and validate each input that has a data-validation attribute, the possible values of which are:

  • alphabetical – any case-insensitive combination of the 26 letters of the English alphabet
  • numeric – any combination of digits between 0 and 9

We will write an end-to-end test to verify the functionality of validateForm against real DOM nodes, as well as against the two validation types we’ll initially support. Once our first implementation works, we will gradually refactor it by writing smaller units, also following TDD.

Continue reading %Learning JavaScript Test-Driven Development by Example%

LEAVE A REPLY