by Shama Hoque June 28, 2020

Getting started with testing your JavaScript application can seem like a daunting task. But with Jest, which is an easy­-to­-use JavaScript testing framework, writing test code for your JavaScript application is quite straight forward. Jest requires almost no configuration to set up, provides many testing features out of the box, and can be used for codebases using different JavaScript libraries and frameworks like React, Angular, Vue, and Node - to name a few!

In this article, we will keep things simple and use Jest to write test code for a frontend­-based to­-do application, developed with basic Javascript and jQuery. While working through this example, we will see how to set up and get started with Jest, and also explore how to test DOM manipulation code and use timer mocks in Jest.

Timed To­-Do application

We will test out the functionality of a timed to-do application built with HTML, CSS, Javascript and jQuery. The interface of the app is simple, it contains an input element that takes the task description and on ENTER key press, adds the task to the list. Each list item displays the done button, the task description text, the start button and the remaining time. Additionally, the count of tasks remaining is shown at the bottom of the list.

Timed To-do App demo

On initial load of this application, the HTML only contains the following elements with the input field and an empty ul tag:

<div class="container">
  <h3>5 Minute To-Dos</h3>
  <input id="addTask" type="text" placeholder="Add a task ..." />
  <ul>
    <!-- Dynamically add list items -->
  </ul>
  <div><span id="count">0</span> tasks left to do</div>
</div>

When the user enters a task, the list item that is dynamically added will have the following structure:

<li>
  <button class="complete notDone"></button>
  <span class="taskText">Task Description</span>
  <button class="startBtn" style="display: none">Start</button>
  <span class="taskTime">04:50</span>
</li>

For each list item containing the task description,

  • the button indicating whether the task is complete or incomplete will change CSS class from notDone to done.
  • the start button will be shown or hidden based on whether the timer was started or not, and if the task is complete.
  • the task time will update when the timer is started and as time runs out.

In order to test the functionality of this application, we will write test code for the following use cases:

  1. User can add a task to the list by typing into the input field and pressing enter
  2. The displayed count of tasks left updates when tasks are done or new tasks are added
  3. User can mark the task as done by clicking on the done check button
  4. User can start a 5 minute timer on a task by clicking the start button
  5. The timer updates with passing time
  6. The task is marked done when time is up

Timed To-do App Tests

Specifically, we will use Jest to add the test cases seen in the screenshot above, to test the JS and jQuery code implementing the to­-do app functionality. Let's go ahead and set up Jest.

The code for this example can be found at https://github.com/shamahoque/timed-ToDo-app-Jest-testing

Setting up Jest

Assuming you already have Node on your system, you can use either npm or yarn to install Jest:

npm install ­­--save­-dev jest

or

yarn add ­­--dev jest

Next, to use Babel with Jest for compiling JavaScript, add babel with

npm install ­­--save­-dev babel­-jest @babel/core @babel/preset­-env

Then, add a babel.config.js file to configure Babel:

module.exports = {
  presets: [["@babel/preset-env"]],
};

Finally, we will update package.json to add a script to run Jest tests:

 "scripts": {
    "test": "jest --coverage"
  }

Jest will now run any test code in the project directory, specifically files that end with .test.js or .spec.js, and files placed in a __tests__ folder, when we run the command:

npm run test

or

yarn test

The --coverage flag will generate code coverage information for the tested code every time the tests are run.

Though it takes zero configuration to get tests running, Jest does provide a handful of configuration options, that can be defined in package.json in a jest configuration object:

"jest": {
    "verbose": true ,
 },

Setting verbose to true will report each individual test in the command line when the tests are run.

To learn more about other available configuration options, check out https://jestjs.io/docs/en/configuration

Writing the test code

With Jest set up, now we can start adding the test code for the to­-do application. Theimplementation code for the to­-do application functionalities are placed in JS files in the scripts folder we will put our Jest tests in corresponding files in the __tests__ folder as follows:

JS file in scripts/Test file in __tests__/
task­-add.jstask­-add.test.js
task­-count.jstask­-count.test.js
task-­done.jstask­-done.test.js
task­-timer.jstask­-timer.test.js

Jest considers each test file to be a test suite, then runs all the test suites when we run Jest.

Initial test setup

For our test suites, before the tests are executed when we run Jest, in a beforeAll() call we will set the initial HTML document body that the tests will run against. We will also require the task­-add.js file here, as it will be used to add new tasks to the list in preparation for the tests.

beforeAll(() => {
  document.body.innerHTML =
    "<div>" +
    '  <input id="addTask" type="text" placeholder="Add a task ..."/>' +
    "  <ul> </ul>" +
    '  <div><span id="count">0</span> tasks left to do</div>' +
    "</div>";

  require("./scripts/task-add");
});

Jest makes the beforeAll(fn, timeout) method available in the global environment, hence it can be used in a test file without importing, and it runs the provided function before all the tests in a test file. Check out the other globals provided by Jest at https://jestjs.io/docs/en/api.

This beforeAll block defined here is needed for all our test suites, so we will place this code in a separate file, testSetup.js, and configure Jest in package.json to run this setup code before every test file:

"jest": {
    "verbose": true,
    "setupFilesAfterEnv": ["<rootDir>/testSetup.js"]
  },

Configuring the setupFilesAfterEnv option makes Jest execute the code in the files specified in the given array, immediately after the test framework has been installed in the environment and before each test file is run.

Adding a new task

In order to test whether a new task is added to the list when a user types into the text input and presses the ENTER key, we will add two test cases to task­-add.test.js.

This test suite will test the functionality implemented in task­-add.js, which essentially adds a keypress event listener to the input element using jQuery. When a keypress event occurs, the listener callback checks if it was the ENTER key, then dynamically generates and adds the new task in a list item. Example code for task­-add.js can be found at task-add.

We will write each test case in an it(name, fn, timeout) method, which is another Jest global. To construct the test specifics and check if values meet expectations, we will use different matchers with expect, like toEqual, toBe, and toHaveBeenCalled, out of the many options available at https://jestjs.io/docs/en/expect.

Let's write the first test case.

Test case: Task is not added to list if a key other than ENTER is pressed

In this test, using jQuery, we first programmatically add text to the input field and trigger a key press ensuring it is not the ENTER key. Then we expect that the initial HTML body will not contain any list items, since a new task was not added. We use the toEqual matcher method to test if the length of list items is 0.

it("does not add task to list if a key other than enter is pressed", () => {
  const $ = require("jquery");
  $("#addTask").val("hello this is a new task");
  let e = $.Event("keypress");
  e.keyCode = 0; // not enter
  $("input").trigger(e);

  expect($("li").length).toEqual(0);
});

Test case: Task is added at the end of the list on ENTER key press in input

Similar to the previous test, we use jQuery to programmatically add text to the input field and this time trigger the ENTER key press. In this case, we expect the last item in the list to have the same task text that was just added in the input.

it("displays a task at the end of the list on enter key press in input", () => {
  const $ = require("jquery");
  $("#addTask").val("hello this is a new task");
  let e = $.Event("keypress");
  e.keyCode = 13; // enter
  $("input").trigger(e);

  expect($("li:last-child .taskText").text()).toEqual(
    "hello this is a new task"
  );
});

Completing a task

A task is marked done either when the user clicks the done check button or when the time is up. When a task is done, the check button CSS is updated, and the start button along with the task time is hidden. We will add three test cases to task­-done.test.js, to check if the done state meets these changes.

This test suite will test the functionality implemented in task­-done.js. Example code for task­-done.js can be found at task-done.

Before each test case runs, we will add a new task to the list using a beforeEach() call. The beforeEach(fn, timeout) method is another global provided by Jest, which runs the given function before each of the tests in the file.

beforeEach(() => {
  const $ = require("jquery");
  $("#addTask").val("hello this is a new task");
  var e = $.Event("keypress");
  e.keyCode = 13; // enter
  $("input").trigger(e);
});

In the function passed to the beforeEach method, we use jQuery to populate the input field and trigger the ENTER key press to add the new task item to the list in the document. With this code, a new list item will be added before each test case runs in this test suite.

Test case: Check if button CSS updates when done button is clicked

In this test, we use jQuery to trigger a click on the done check button in the last list item added to the list. Then we expect this button to have the CSS class done and also to be disabled. We use the toBe matcher method to test if the jQuery hasClass method and prop('disabled') return true.

it("displays disabled done button css when task complete button clicked", () => {
  const $ = require("jquery");
  $("li:last-child .complete").click();

  expect($("li:last-child .complete").hasClass("done")).toBe(true);
  expect($("li:last-child .complete").prop("disabled")).toBe(true);
});

Test case: Start button is hidden when task is done

Similar to the previous test, we trigger a click on the done check button in the last item added and expect the start button to be hidden with CSS.

it("hides start button when task done", () => {
  const $ = require("jquery");
  $("li:last-child .complete").click();

  expect($("li:last-child .startBtn").css("display")).toEqual("none");
});

Test case: Task time is hidden when task is done

In this test, we first trigger a click on the start button in the last item added and expect the task time element to be added and displayed on this list item. We use a .not.toEqual() combination to check that the display CSS value is not set to none on the task time element. Then, we trigger a click on the done check button on this list item, and now expect the display CSS to equal none on the task time element.

it("hides time when task done", () => {
  const $ = require("jquery");
  $("li:last-child .startBtn").click();
  expect($("li:last-child .taskTime").length).toEqual(1);
  expect($("li:last-child .taskTime").css("display")).not.toEqual("none");

  $("li:last-child .complete").click();
  expect($("li:last-child .taskTime").css("display")).toEqual("none");
});

Timing a task

When a user clicks the start button next to a task, a 5­-minute timer is started which shows the remaining time, and the task is marked as done when the time is up. We will add three test cases to task­-timer.test.js, to check these timing functionalities.

This test suite will test the functionality implemented in task­-timer.js. Example code for task­-timer.js can be found at task-timer.

Before each test runs, we will add a new task to the list and trigger the click on its start button to initiate the timer, using a beforeEach() call.

beforeEach(() => {
  $("#addTask").val("hello this is a new task");
  var e = $.Event("keypress");
  e.keyCode = 13; // enter
  $("input").trigger(e);
  $("li:last-child .startBtn").click();
});

The timer implementation code uses setInterval to display time updates. In order to mock the behavior of setInterval for testing, we will use fake timers from Jest. We will also mock the doneTask function, so we can check if it is called when time is up.

jest.useFakeTimers();
jest.mock("../scripts/task-done.js");

The call to jest.useFakeTimers() enables fake timers and mocks out the setInterval function. This mock will allow us to control the passage of time and check the change in the task time as a result. Check out Timer Mocks to explore more capabilities of using fake timers in Jest at https://jestjs.io/docs/en/timer-mocks

Calling jest.mock() with task­-done.js mocks the doneTask function exported in this file, and this will let us spy on whether the doneTask function is invoked when a task's time is up.

The Mock Functions API in Jest provides more ways to use mock functions, learn more at mock-function-api.

Test case: Timer is started and displayed after start button clicked

In this test, since the start button was already clicked in the beforeEach, we expect the start button to be hidden by CSS, the task time element to be added to the list item, and setInterval to have been called, which will indicate that the timer was started. We use the toHaveBeenCalled matcher to check if setInterval was called.

it("starts timer for a task when the start button is clicked", () => {
  expect($("li:last-child .startBtn").css("display")).toEqual("none");
  expect($("li:last-child .taskTime").length).toEqual(1);
  expect(setInterval).toHaveBeenCalled();
});

Test case: Timer updates after some time passes

In this test, we use the advanceTimersByTime API from Jest to let a minute of time pass, so we can test if the task time is displayed accurately after the passage of this time. The task time starts at 05:00 when the setInterval is invoked on start button click. Then after a minute passes, the expected value is 04:00.

it("shows remaining time after a certain time has passed", () => {
  jest.advanceTimersByTime(61000);
  expect($("li:last-child .taskTime").text()).toEqual("04:00");
});

Test case: Task is done when time is up

In this test, we advance time by 5 minutes, and expect task time to become 00:00, clearInterval to have been called, and doneTask to have been called.

it("marks task as done when time up", () => {
  const doneTask = require("../scripts/task-done.js");
  jest.advanceTimersByTime(301000);
  expect($("li:last-child .taskTime").text()).toEqual("00:00");
  expect(clearInterval).toHaveBeenCalled();
  expect(doneTask).toHaveBeenCalled();
});

We are able to spy on clearInterval and doneTask here because we are using fake timers and we mocked out the doneTask function.

Updating count of tasks left

The count of tasks left to complete is updated in three scenarios, first when a new task is added to the list, next when a task is clicked as done, and then when the started time on a task is up.

This test suite will test the functionality implemented in task­-count.js. Example code for task­-count.js can be found at task-count.js.

In task-count.test.js, we will group tests for these three cases in a describe block. The describe(name, fn) method is made globally available by Jest, and it allows to organize blocks of related tests into groups.

describe("displayed count of tasks left", () => {
  /* test cases */
});

Inside the describe block, we will first add a beforeAll, which will add a task to the empty list so the count starts at 1:

beforeAll(() => {
  $("#addTask").val("Task 1");
  var e = $.Event("keypress");
  e.keyCode = 13; // enter
  $("input").trigger(e);
});

Test case: Displayed count increments by 1 when a new task is added

This test will add a new task to the list and expect the count text to be equal to 2.

it("increments when new task added", () => {
  $("#addTask").val("Task 2");
  var e = $.Event("keypress");
  e.keyCode = 13; // enter
  $("input").trigger(e);

  expect($("#count").text()).toEqual("2");
});

Test case: The displayed count decreases by 1 when a task is marked as done

In this test, we trigger a click on the done check button and expect count text to be equal to 1.

it("decrements when a task is done", () => {
  $("li:last-child .complete").click();

  expect($("#count").text()).toEqual("1");
});

Test case: The displayed count decreases by 1 when a timer started on a task passes 5 minutes

For this test, we once again use fake timers to mock out the setInterval function and advance time by 5 minutes. Then we expect the count text to be equal to 0.

jest.useFakeTimers();
it("decrements when a task time is up", () => {
  $("li:first-child .startBtn").click();
  jest.advanceTimersByTime(303000);

  expect($("#count").text()).toEqual("0");
});

With these tests, we have covered testing for the core functionalities like adding tasks, marking tasks as done, timing tasks, and keeping count of remaining tasks in the frontend based to­-do application.

While writing the test code for these functionalities, we touched on the following Jest topics:

  • Setting up and configuring Jest
  • Configuring Jest to run initial test setup code before running the test suites
  • Testing DOM manipulation code using jQuery to trigger DOM events
  • Using Jest globals like beforeAll, beforeEach, it, and describe
  • Using expect with matchers like toEqual, toBe, and toHaveBeenCalled
  • Mock Functions API to spy on functions called by the code being tested
  • Using fake timers to mock timing functions like setInterval, and clearInterval, and how to control passage of time with these mocks.

There's a whole lot more that can be done with Jest for testing JavaScript applications. You can expand on the practical examples discussed in this article to explore more possibilities with Jest.

Author: Shama Hoque

Shama Hoque

Shama Hoque is a software developer, author, and mentor with more than 9 years of experience. Currently, she makes web-based prototypes for R&D startups in California, while training aspiring software engineers and teaching web development to CS undergrads in Bangladesh. She is the author of Packt's Full-Stack React Projects book.