Testing with Jest: Start with a basic To-Do application
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.
Image loading...
On initial load of this application, the HTML only contains the following elements with the input
field and an empty ul
tag:
html<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:
html<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
todone
. - 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:
- User can add a task to the list by typing into the input field and pressing enter
- The displayed count of tasks left updates when tasks are done or new tasks are added
- User can mark the task as done by clicking on the done check button
- User can start a 5 minute timer on a task by clicking the start button
- The timer updates with passing time
- The task is marked done when time is up
Image loading...
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:
bashnpm install --save-dev jest
$$
or
bashyarn add --dev jest
$$
Next, to use Babel with Jest for compiling JavaScript, add babel with
bashnpm install --save-dev babel-jest @babel/core @babel/preset-env
$$
Then, add a babel.config.js
file to configure Babel:
javascriptmodule.exports = { presets: [["@babel/preset-env"]], };
Finally, we will update package.json
to add a script to run Jest tests:
javascript"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:
bashnpm run test
$$
or
bashyarn 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:
javascript"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.js | task-add.test.js |
task-count.js | task-count.test.js |
task-done.js | task-done.test.js |
task-timer.js | task-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.
jsbeforeAll(() => { 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:
js"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
.
jsit("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.
javascriptit("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.
javascriptbeforeEach(() => { 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
.
javascriptit("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.
javascriptit("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.
javascriptit("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.
javascriptbeforeEach(() => { $("#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.
javascriptjest.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.
javascriptit("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
.
javascriptit("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.
javascriptit("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.
javascriptdescribe("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:
javascriptbeforeAll(() => { $("#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
.
javascriptit("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
.
javascriptit("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
.
javascriptjest.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
, anddescribe
- Using
expect
with matchers liketoEqual
,toBe
, andtoHaveBeenCalled
- Mock Functions API to spy on functions called by the code being tested
- Using fake timers to mock timing functions like
setInterval
, andclearInterval
, 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.