DEV Community

loading...
Cover image for How to write unit tests in JavaScript with Jest

How to write unit tests in JavaScript with Jest

Domagoj Štrekelj
Engineer and teacher. Full-stack generalist. Lifelong learning advocate. Aiming to contribute to an interactive and inclusive web.
Updated on ・9 min read

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));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
    }
}
Enter fullscreen mode Exit fullscreen mode

We can now run the newly created test script:

# NPM users
npm run test

# Yarn users
yarn run test
Enter fullscreen mode Exit fullscreen mode

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. Seeing npm 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 called test or spec).

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);
});
Enter fullscreen mode Exit fullscreen mode

💡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:

  1. isInteger() passes for integer value;
  2. 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
The test() 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);
});
Enter fullscreen mode Exit fullscreen mode

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)
);
Enter fullscreen mode Exit fullscreen mode

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)
    );
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

💡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 run jest --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
Enter fullscreen mode Exit fullscreen mode

The unit to be tested in isInteger.js:

// isInteger.js
module.exports = (value) => !isNaN(parseInt(value, 10));
Enter fullscreen mode Exit fullscreen mode

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)
    );
});
Enter fullscreen mode Exit fullscreen mode

The test script in package.json:

jest --watchAll --coverage
Enter fullscreen mode Exit fullscreen mode

Homework and next steps

  • Write more comprehensive tests. How are strings handled? Objects? null and undefined? 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!

Discussion (7)

Collapse
chiaralyn profile image
ChiaraLyn

I have recently discovered the importance of TDD and will soon study how to do it. These guides are invaluable and help me so much to understand the dynamics behind good code. This article is saved in my favorites, thank you very much.

Collapse
dstrekelj profile image
Domagoj Štrekelj Author • Edited

Hi @chiaralyn ! Thank you for the comment :)

It's very motivating to hear you saved the article to your favorites. I'm happy you found it helpful and enjoyable!

There's a new article in this series coming up next week where we'll talk about mocking imported functions with Jest. This will also be our first look into writing solitary unit tests for sociable units. I'd love to hear your thoughts on it once it's published :)

Collapse
chiaralyn profile image
ChiaraLyn

I can't wait to read the new series of articles. It would be really useful and interesting. Thank you so much for your efforts and your kindness!

Collapse
juanfabiorey profile image
juanfabiorey

Oh great! Thank you!

Collapse
dstrekelj profile image
Domagoj Štrekelj Author

You're welcome juanfabiorey :)

Collapse
mac10046 profile image
Abdeali • Edited

I have not written any test code in js yet, but your article is nice and simple and also credits to jest, I look forward to write some test code soon

Collapse
dstrekelj profile image
Domagoj Štrekelj Author

Thank you for the comment Abdeali!

Glad to hear you're going to try your hand at writing tests. Let me know how it goes! :)