How to write unit tests in JavaScript with Jest
Domagoj Štrekelj
Posted on July 24, 2021
Unit testing is an important and often overlooked part of the development process. It is considered boring by many, and being traditionally difficult to properly set up earned it a poor reputation early on. The benefits of shipping quality code certainly outweigh any negatives, but how does one find the time and muster the effort to start writing unit tests?
Lucky for us, writing unit tests in JavaScript has never been faster, easier, and arguably more fun thanks to Jest.
Jest is a feature-rich JavaScript testing framework that aims to bring testing to the masses. It's near-zero configuration approach makes it simple to set up, and a familiar API makes writing tests fairly straightforward.
This article will provide a brief introduction into Jest and the concepts behind unit testing. We will learn how to install Jest, write test suites with test cases and fixtures, and run tests both with and without coverage reports.
We will assume that we're testing a module containing a simple function behaving as a validation rule. The rule checks whether the validated value is an integer number. For example:
// isInteger.js
module.exports = (value) => !isNaN(parseInt(value, 10));
This implementation is naive and faulty on purpose. We want to see what our tests will teach us about the flaws in our code by passing and failing test cases. Fixing the implementation is not covered by this article, but feel free to play with it as we move through it.
Read on to find out more!
What is a unit test?
A unit test is an automated test of a unit of source code. A unit test asserts if the unit's behaviour matches expectations.
A unit is usually a line of code, function, or class. There is no strict definition of what makes up a unit, but it's common to start with whatever seems "smallest".
Units that have no dependencies are called isolated (solitary) units. Units that have dependencies are called sociable units.
Solitary units are easy to test, but sociable units are more difficult. The output of a sociable unit depends on other units of code - if other units fail, the tested unit fails as well. This created two unit test styles: sociable unit tests and solitary unit tests.
Sociable unit tests fail if the dependencies of a sociable unit are also failing. The tested unit is not supposed to work if it's dependencies don't work, so a failing test in this case is a good sign.
Solitary unit tests isolate sociable units by creating mock implementations of their dependencies. Mocks control how dependencies behave during tests, making sociable units predictable to test.
No matter the unit test style, the goal of unit testing remains the same - to ensure that individual parts of the program are working correctly as expected.
What is Jest?
Jest is a JavaScript testing framework designed to make testing as easy as possible. It provides all the essential tools for running tests, making assertions, mocking implementations, and more in a single package.
Before Jest, the JavaScript ecosystem relied on several different tools and frameworks to give developers a way to write and run tests. Configuring these tools was rarely simple and easy. Jest aims to fix that by using sensible default configurations that work "out of the box", with little to no additional configuration required in most cases.
Jest is currently one of the most popular testing technology choices, consistently earning high satisfaction marks in the State of JS developer survey since 2017. It's the reliable choice for testing JavaScript projects.
💡Note
Jest also supports TypeScript via Babel.
How to install Jest?
Install the jest
package (and optional typings) to a new or existing project's package.json
file using your package manager of choice:
# For NPM users
npm install --save-dev jest @types/jest
# Yarn users
yarn add --dev jest @types/jest
That's it! We're now ready to run tests with Jest.
💡Note
It's good practice to install Jest and any other testing tools as development dependencies. This speeds up installation in environments where only dependencies required for the project to build and run are installed.
How to run tests with Jest?
To run tests with Jest call the jest
command inside the root of the project folder.
We will update the project's package.json
with a test script that calls the jest
command for us:
{
// ... package.json contents
"scripts": {
// ... existing scripts
"test": "jest"
}
}
We can now run the newly created test
script:
# NPM users
npm run test
# Yarn users
yarn run test
If everything is set up correctly Jest will give us the results of any tests it found and ran.
💡Note
Jest exits with status code 1 when a test case fails. Seeingnpm ERR!
errors in the console is expected in this case.
How to create a test with Jest?
To create a test for use with Jest we create a *.spec.js
or *.test.js
file that will contain our test cases.
💡Note
Jest is configured by default to look for.js
,.jsx
,.ts
and.tsx
files inside of__tests__
folders, as well as any files with a suffix of.test
or.spec
(this includes files calledtest
orspec
).
Since isInteger.js
is the name of the module we're testing, we will write our tests in an isInteger.spec.js
file created in the same folder as the module:
// isInteger.spec.js
test("Sanity check", () => {
expect(true).toBe(true);
});
💡Note
Whether you choose to write tests inside a dedicated folder or right next to your modules, there is no right or wrong way to structure tests inside a project. Jest is flexible enough to work with most project architectures without configuration.
The test's description is "Sanity check". Sanity checks are basic tests to ensure the system behaves rationally. The test will assert that we expect the value true
to be true
.
Run the test and if it passes everything is set up correctly.
Congratulations! We just wrote our first test!
How to write a test case in Jest?
To write a test case we first define the outcomes that we must validate to ensure that the system is working correctly.
The isInteger.js
module is a function that takes one parameter and returns true
if the parameter is an integer value or false
if it isn't. We can create two test cases from that definition:
-
isInteger()
passes for integer value; -
isInteger()
fails for non-integer value.
To create a test case in Jest we use the test()
function. It takes a test name string and handler function as the first two arguments.
💡Note
Thetest()
function can also be called under the alias -it()
. Choose one over the other depending on readability or personal preference.
Tests are based on assertions. Assertions are made up of expectations and matchers. The simplest and most common assertion expects the tested value to match a specific value.
An expectation is created with the expect()
function. It returns an object of matcher methods with which we assert something expected about the tested value. The matcher method toBe()
checks if the expectation matches a given value.
In our tests, we can expect isInteger()
to be true
for the integer value 1, and false
for the non-integer value 1.23.
// isInteger.spec.js
const isInteger = require("./isInteger");
test("isInteger passes for integer value", () => {
expect(isInteger(1)).toBe(true);
});
test("isInteger fails for non-integer value", () => {
expect(isInteger(1.23)).toBe(false);
});
Running Jest should now give us a report on which tests pass, and which tests fail.
How to use fixtures in Jest?
To use fixtures in Jest we can use the test.each()
function. It performs a test for each fixture in an array of fixtures.
Fixtures are data representing conditions - such as function arguments and return values - under which the unit test is performed. Using fixtures is a quick and easy way to assert that a unit's behaviour matches expectations under different conditions without having to write multiple tests.
In Jest, a fixture can be a single value or an array of values. The fixture is available in the test handler function through parameters. The value or values of a fixture can be injected in the description through printf formatting.
// isInteger.spec.js
const isInteger = require("./isInteger");
const integerNumbers = [-1, 0, 1];
test.each(integerNumbers)(
"isInteger passes for integer value %j",
(fixture) => expect(isInteger(fixture)).toBe(true)
);
// ... or...
const integerNumbers = [
[-1, true],
[-0, true],
[1, true]
];
test.each(integerNumbers)(
"isInteger passes for integer value %j with result %j",
(fixture, result) => expect(isInteger(fixture)).toBe(result)
);
Running Jest should now give us a report on which tests pass, and which tests fail, where every test corresponds to a fixture from our array of fixtures.
💡Note
%j
is a printf formatting specifier that prints the value as JSON. It's a good choice for fixtures that contain values of different types.
How to group test cases in Jest into a test suite?
To group test cases in Jest into a test suite we can use the describe()
function. It takes a suite name string and handler function as the first two arguments.
A test suite is a collection of test cases grouped together for execution purposes. The goal of a test suite is to organise tests by common behaviour or functionality. If all tests within a suite pass, we can assume that the tested behaviour or functionality meets expectations.
// isInteger.spec.js
const isInteger = require("./isInteger");
describe("isInteger", () => {
const integerNumbers = [-10, -1, 0, 1, 10];
test.each(integerNumbers)(
"passes for integer value %j",
(fixture) => expect(isInteger(fixture)).toBe(true)
);
const floatNumbers = [-10.1, -1.1, 0.1, 1.1, 10.1];
test.each(floatNumbers)(
"fails for non-integer value %j",
(fixture) => expect(isInteger(fixture)).toBe(false)
);
});
Running Jest should now give us a report on which tests pass, and which tests fail, grouped into described test suites.
💡Note
describe()
blocks can also be nested to create more complex test hierarchies.
How to run Jest every time files change?
To run Jest every time files change we can use the --watch
and --watchAll
flags.
The --watch
flag will tell Jest to watch for changes in files tracked by Git. Jest will run only those tests affected by the changed files. For this to work, the project must also be a Git repository.
The --watchAll
flag will tell Jest to watch all files for changes. Whenever a file changes, Jest will run all tests.
Both --watch
and --watchAll
modes support additional filtering of tests while the tests are running. This makes it possible to only run tests matching a file name, or only run failing tests.
# Runs tests on changed files only and re-runs for any new change
# Note: the project must also be a git repository
jest --watch
# Runs tests on all files and re-runs for any new change
jest --watchAll
How to get a test coverage report with Jest?
To get a test coverage report with Jest we can use the --coverage
flag.
Test coverage is a software testing metric that describes how many lines of source code (statements) of the tested unit are executed (covered) by tests. A test coverage of 100% for a unit means every line of code in the unit has been called by the test.
We should always aim for a high test coverage - ideally 100% - but also keep in mind that total coverage does not mean we tested all cases, only lines of code.
# Runs tests and prints a test coverage afterwards
jest --coverage
💡Note
We can combine different flags to get more features out of Jest. For example, to watch all files and get a coverage report we can runjest --watchAll --coverage
.
With that we're all set! We can now write tests and run them whenever a file changes, and also review test coverage reports for covered and uncovered lines of code.
Jest unit test example code
To install Jest:
# For NPM users
npm install --save-dev jest @types/jest
# Yarn users
yarn add --dev jest @types/jest
The unit to be tested in isInteger.js
:
// isInteger.js
module.exports = (value) => !isNaN(parseInt(value, 10));
The unit test in isInteger.spec.js
:
// isInteger.spec.js
const isInteger = require("./isInteger");
describe("isInteger", () => {
const integerNumbers = [-10, -1, 0, 1, 10];
test.each(integerNumbers)(
"passes for integer value %j",
(fixture) => expect(isInteger(fixture)).toBe(true)
);
const floatNumbers = [-10.1, -1.1, 0.1, 1.1, 10.1];
test.each(floatNumbers)(
"fails for non-integer value %j",
(fixture) => expect(isInteger(fixture)).toBe(false)
);
});
The test script in package.json
:
jest --watchAll --coverage
Homework and next steps
- Write more comprehensive tests. How are strings handled? Objects?
null
andundefined
? Consider adding more fixtures to cover these cases. - Fix the code so the tests pass or write a newer, better implementation.
- Achieve 100% code coverage in the coverage report.
Thank you for taking the time to read through this article!
Have you tried writing unit tests in Jest before? How do you feel about Jest?
Leave a comment and start a discussion!
Posted on July 24, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.