DEV Community

Cover image for How to Test JavaScript With Jest
Thomas Lombart
Thomas Lombart

Posted on • Edited on

How to Test JavaScript With Jest

Testing is an important skill every developer should have. Still, some developers are reluctant to test. We’ve all met at some point someone who thinks tests are useless or that it takes too much effort to write them. While it's possible to have that feeling when starting to write tests, once you learn to properly test your apps, you'll never look back again. Why? Because when well-written, tests allow you to ship robust apps with confidence.

Testing is essential

Let's assume you're working on a brand new app. You've been coding for weeks or months, so you master your code. You know every part of it. So why should you write tests on things you already know?

Well, the more your codebase grows, the harder it is to maintain it. There's always a point when you break your code as you add new features. Then you have to start debugging, modify your existing code, and hope your fix doesn't break any other features. If it does, you’ll think: "I'm sick of this app! I can't even ship one tiny feature without breaking something!".

Let's take another example. You land on an existing codebase without tests. Same thing here: good luck adding new features without regressing!

But what if you're working with other developers? What if you don't have any other choices than just fixing the app? You'll be entering the reboot phase: the moment when you decide to rebuild all your existing features because you're not sure anymore of what's going on.

The solution to both of these examples is to write tests. It may seem like a waste of time now, but it'll actually be a time-saver later. Here are some main benefits that come along with writing tests:

  • You can refactor your code without breaking anything because tests are here to tell you if something wrong happened.
  • You can ship new features confidently without any regression.
  • Your code becomes more documented because we can see what the tests do. You spend less time testing your app and more time on working on what's essential.

So, yes, writing tests takes time. Yes, it’s hard at first. Yes, building the app sounds more fun. But I'll say it again: writing tests is essential and saves time when implemented correctly.

In this article, we'll discover a powerful tool to write tests for JavaScript apps: Jest.

Discover Jest

In a nutshell, Jest is an all-in-one JavaScript testing tool built by Facebook. Why all-in-one? Well, because with Jest only, you can do all of these things:

  • Run your tests safely and fast
  • Make assertions on your code
  • Mock functions and modules
  • Add code coverage
  • Snapshot testing
  • And more!

While it's true you can use other testing tools like Mocha, Chai or Sinon, I prefer to use Jest for its simplicity of use.

Installation

To add Jest, nothing more simple than adding a package in your project:

npm install --save-dev jest
Enter fullscreen mode Exit fullscreen mode

Then you can add a test script in your package.json file:

{
  "scripts": {
    "test": "jest"
  }
}
Enter fullscreen mode Exit fullscreen mode

Running jest by default will find and run files located in a __tests__ folder or ending with .spec.js or .test.js.

Structure of a test file

Jest provides functions to structure your tests:

  • describe: used for grouping your tests and describing the behavior of your function/module/class. It takes two parameters. The first one is a string describing your group. The second one is a callback function in which you have your test cases or hook functions (more on that just below 😉).
  • it or test: it's your test case, that is to say, your unit test. It must be descriptive. The parameters are exactly the same as describe.
  • beforeAll (afterAll): hook function that runs before (after) all tests. It takes one parameter: the function you will run before (after) all tests.
  • beforeEach (afterEach): hook function that runs before (after) each test. It takes one parameter: the function you will run before (after) each test.

Notes:

  • beforeAll, beforeEach, and other hook functions are called so because they allow you to call your own code and modify the behavior of your tests.
  • It's possible to skip (ignore) tests by using .skip on describe and it: it.skip(...) or describe.skip(...).
  • You can select exactly which tests you want to run by using .only on describe and it: it.only(...) or describe.only(...). It is useful if you have a lot of tests and want to focus on only one test.

A first test

describe("My first test suite", () => {
  it("adds two numbers", () => {
    expect(add(2, 2)).toBe(4);
  });

  it("substracts two numbers", () => {
    expect(substract(2, 2)).toBe(0);
  });
});
Enter fullscreen mode Exit fullscreen mode

Matchers

When you write a test, you usually need to make assertions on your code. For example, you would expect an error to appear on the screen if a user gives the wrong password on a login screen. More generally, to make an assertion, you need an input and an expected output. Jest allows us to do that easily by providing matchers to test our values:

expect(input).matcher(output);
Enter fullscreen mode Exit fullscreen mode

Here are the most common one:

  • toBe: compares primitive values (boolean, number, string) or the references of objects and arrays (aka referential equality)
expect(1 + 1).toBe(2);

const firstName = "Thomas";
const lastName = "Lombart";
expect(`${firstName} ${lastName}`).toBe("Thomas Lombart");

const testsAreEssential = true;
expect(testsAreEssential).toBe(true);
Enter fullscreen mode Exit fullscreen mode
  • toEqual: compares all properties of arrays or objects (aka deep equality) recursively.
const fruits = ["banana", "kiwi", "strawberry"];
const sameFruits = ["banana", "kiwi", "strawberry"];
expect(fruits).toEqual(sameFruits);
// Oops error! They don't have the same reference
expect(fruits).toBe(sameFruits);

const event = {
  title: "My super event",
  description: "Join me in this event!",
};

expect({ ...event, city: "London" }).toEqual({
  title: "My super event",
  description: "Join me in this event!",
  city: "London",
});
Enter fullscreen mode Exit fullscreen mode
  • toBeTruthy (toBeFalsy): tells if the value is true (false).
expect(null).toBeFalsy();
expect(undefined).toBeFalsy();
expect(false).toBeFalsy();

expect("Hello world").toBeTruthy();
expect({ foo: "bar" }).toBeTruthy();
Enter fullscreen mode Exit fullscreen mode
  • not: has to be placed in front of a matcher and returns the opposite of the matcher's result.
expect(null).not.toBeTruthy();
// same as expect(null).toBeFalsy()

expect([1]).not.toEqual([2]);
Enter fullscreen mode Exit fullscreen mode
  • toContain: checks if the array contains the element in parameter
expect(["Apple", "Banana", "Strawberry"]).toContain("Apple");
Enter fullscreen mode Exit fullscreen mode
  • toThrow: checks if a function throws an error
function connect() {
  throw new ConnectionError();
}

expect(connect).toThrow(ConnectionError);
Enter fullscreen mode Exit fullscreen mode

They are not the only matchers, far from there. You can also discover in the Jest docs toMatch, toBeGreaterThan, toBeUndefined, toHaveProperty and much more!

Jest CLI

We covered the structure of a test file and the matchers provided by Jest. Let's see how we can use its CLI to run our tests.

Run tests

Let's recall what we saw in Discover Jest's lesson: running only jest. By default jest will lookup at the directory's root and run all files located in a __tests__ folder or ending with .spec.js or .test.js.

You can also specify the filename of the test file you want to run or a pattern:

jest Event # run all test files containing Event
jest src/EventDetail.test.js # run a specific file
Enter fullscreen mode Exit fullscreen mode

Now let's say you want to run a specific test, Jest allows you to do so with the -t option. For example, consider the two following test suites:

describe("calculator", () => {
  it("adds two numbers", () => {
    expect(2 + 2).toBe(4)
  })

  it("substracts two numbers", () => {
    expect(2 - 2).toBe(0)
  })

  it("computes something", () => {
    expect(2 * 2).toBe(4)
  })
})
Enter fullscreen mode Exit fullscreen mode
describe("example", () => {
  it("does something", () => {
    expect(foo()).toEqual("bar")
  })

  it("does another thing", () => {
    const firstName = "John"
    const lastName = "Doe"
    expect(`${firstName} ${lastName}`).toBe("John Doe")
  })
})
Enter fullscreen mode Exit fullscreen mode

By running the following command:

jest -t numbers
Enter fullscreen mode Exit fullscreen mode

Jest will run the first two tests of calculator.test.js but will skip the rest.

Watch mode

Then there's, what I think is, the handiest option of Jest: watch mode. This mode watches files for changes and reruns the tests related to them. To run it, you just have to use the --watch option:

jest --watch
Enter fullscreen mode Exit fullscreen mode

Note: Jest knows what files are changed thanks to Git. So you must enable git in your project to make use of that feature.

Coverage

Let's see the last option to show you how powerful Jest is: collecting test coverage, that is to say, the measurement of the amount of code covered by a test suite when run. This metric can be useful to make sure your code is properly covered by your tests. To make use of that, run the following command:

jest --coverage
Enter fullscreen mode Exit fullscreen mode

Note: striving for 100% coverage everywhere doesn't make sense, especially for UI testing (because things move fast). Reach 100% coverage for things that matter most, like a module or component related to payments.

If I gave you every possible option provided by Jest CLI, this article would take you forever, so if you want to learn more about them, look at their docs.

Mocks

A mock is a fake module that simulates the behavior of a real object. Put it another way, mocks allow us to fake our code to isolate what we are testing.

But why would you need mocks in your tests? Because in real-world apps, you depend on many things such as databases, third-party APIs, libraries, other components, etc. However, you usually don't want to test what your code depends on, right? You can safely assume that what your code uses works well. Let's take two examples to illustrate the importance of mocks:

  1. You want to test a TodoList component that fetches your todos from a server and displays them. Problem: you need to run the server to fetch them. If you do so, your tests will get both slow and complicated.
  2. You have a button that, when clicked, selects a random image among ten other images. Problem: you don't know in advance which image is going to be chosen. The best you can do is to make sure the selected image is one of the ten images. Thus, you need your test to be deterministic. You need to know in advance what will happen. And you guessed it, mocks can do that.

Mock functions

You can easily create mocks with the following function:

jest.fn();
Enter fullscreen mode Exit fullscreen mode

It doesn't look like so, but this function is really powerful. It holds a mock property that makes it possible for us to keep track of how many times the functions have been called, which arguments, the returned values, etc.

const foo = jest.fn();
foo();
foo("bar");
console.log("foo", foo); // foo ƒ (){return e.apply(this,arguments)}
console.log("foo mock property", foo.mock); // Object {calls: Array[2], instances: Array[2], invocationCallOrder: Array[2], results: Array[2]}
console.log("foo calls", foo.mock.calls); // [Array[0], Array[1]]
Enter fullscreen mode Exit fullscreen mode

In this example, you can see that because foo has been called twice, calls have two items representing the arguments passed in both function calls. Thus, we can make assertions on what were passed to the function:

const foo = jest.fn();
foo("bar");

expect(foo.mock.calls[0][0]).toBe("bar");
Enter fullscreen mode Exit fullscreen mode

Writing such an assertion is a little bit tedious. Luckily for us, Jest provides useful matchers when it comes to make mock assertions such as toHaveBeenCalled, toHaveBeenCalledWith, toHaveBeenCalledTimes and much more:

const hello = jest.fn();
hello("world");
expect(hello).toHaveBeenCalledWith("world");

const foo = jest.fn();
foo("bar");
foo("hello");
expect(foo).toHaveBeenCalledTimes(2);
expect(foo).toHaveBeenNthCalledWith(1, "bar");
expect(foo).toHaveBeenNthCalledWith(2, "hello");
// OR
expect(foo).toHaveBeenLastCalledWith("hello");
Enter fullscreen mode Exit fullscreen mode

Let's take a real-world example: a multi-step form. On each step, you have form inputs and also two buttons: previous and next. Clicking on previous or next triggers a saveStepData(nextOrPreviousFn) function that, well, saves your data and executes the nextOrPreviousFn callback function, which redirects you to the previous or next step.

Let's say you want to test the saveStepData function. As said above, you don't need to care about nextOrPreviousFn and its implementation. You just want to know it has correctly been called after saving. Then you can use a mock function to do so. This useful technique is called dependency injection:

function saveStepData(nextOrPreviousFn) {
  // Saving data...
  nextOrPreviousFn();
}

const nextOrPreviousMock = jest.fn();
saveStepData(nextOrPreviousMock);
expect(nextOrPreviousMock).toHaveBeenCalled();
Enter fullscreen mode Exit fullscreen mode

So far, we know how to create mocks and if they have been called. But what if we need to change the implementation of a function or modify the returned value to make one of our test deterministic? We can do so with the following function:

jest.fn().mockImplementation(implementation);
// Or with the shorthand
jest.fn(implementation);
Enter fullscreen mode Exit fullscreen mode

Let's try it right away:

const foo = jest.fn().mockImplementation(() => "bar");
const bar = foo();

expect(foo.mock.results[0].value).toBe("bar");
// or
expect(foo).toHaveReturnedWith("bar");
// or
expect(bar).toBe("bar");
Enter fullscreen mode Exit fullscreen mode

In this example, you can see we could mock the returned value of the foo function. Thus, the variable bar holds the "bar" string.

Note: It's also possible to mock asynchronous functions using mockResolvedValue or mockRejectedValue to respectively resolve or reject a Promise.

Mock modules

Sure, we can mock functions. But what about modules, you might think? They're important as well since we import them in mostly every component! Don't worry, Jest got you covered with jest.mock.

Using it is quite simple. Just give it the path of the module you want to mock, and then everything is automatically mocked.

For instance, let's take the case of axios, one of the most popular HTTP clients. Indeed, you don't want to perform actual network requests in your tests because they could get very slow. Let's mock axios then:

import axiosMock from "axios";

jest.mock("axios");
console.log(axiosMock);
Enter fullscreen mode Exit fullscreen mode

Note: I named the module axiosMock and not axios for readability reasons. I want to make it clear it's a mock and not the real module. The more readable, the better!

With jest.mock the different axios functions such as get, post, etc are mocked now. Thus, we have full control over what axios sends us back:

import axiosMock from "axios";

async function getUsers() {
  try {
    // this would typically be axios instead of axiosMock in your app
    const response = await axiosMock.get("/users");
    return response.data.users;
  } catch (e) {
    throw new Error("Oops. Something wrong happened");
  }
}

jest.mock("axios");

const fakeUsers = ["John", "Emma", "Tom"];
axiosMock.get.mockResolvedValue({ data: { users: fakeUsers } });

test("gets the users", async () => {
  const users = await getUsers();
  expect(users).toEqual(fakeUsers);
});
Enter fullscreen mode Exit fullscreen mode

Another great feature of Jest is shared mocks. Indeed, if you were to reuse the axios mock implementation above, you could just create a __mocks__ folder alongside the node_modules folder with a axios.js file in it:

module.exports = {
  get: () => {
    return Promise.resolve({ data: { users: ["John", "Emma", "Tom"] } });
  },
};
Enter fullscreen mode Exit fullscreen mode

And then in the test:

import axiosMock from "axios"

// Note that we still have to call jest.mock!
jest.mock("axios")

async function getUsers() {
  try {
    const response = await axios.get("/users")
    return response.data.users
  } catch (e) {
    throw new Error("Oops. Something wrong happened")
  }
}

test("gets the users", async () => {
  const users = await getUsers()
  expect(users.toEqual(["John", "Emma", "Tom"]))
}
Enter fullscreen mode Exit fullscreen mode

Configure Jest

It's not because Jest works out of the box that it can't be configured, far from it! There are a lot of configuration options for Jest. You can configure Jest in three different ways:

  1. Via the jest key in package.json (same as eslintConfig or prettier keys if you read my last article)
  2. Via jest.config.js
  3. Via any json or js file using jest --config.

Most of the time, you'll use the first and second one.

Let's see how to configure Jest for a React app, especially with Create React App (CRA)

Indeed if you're not using CRA, you'll have to write your own configuration. Because it partly has to do with setting up a React app (Babel, Webpack, etc), I won't cover it here. Here is a link from Jest docs directly that explains the setup without CRA.

If you're using CRA, you have nothing to do, Jest is already setup (though it's possible to override the configuration for specific keys).

However, it's not because CRA setups Jest for you that you shouldn't know how to set up it. Thus, you'll find below common Jest configuration keys that you'll likely use or see in the future. You'll also see how CRA is using them.

Match test files

You can specify a global pattern to tell Jest which tests to run thanks to the testMatch key. By default CRA uses the following:

{
  "testMatch": [
    "<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
    "<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
  ]
}
Enter fullscreen mode Exit fullscreen mode

This pattern means that Jest will run tests on .js, jsx, ts and tsx files located in src that are either in a __tests__ folder or if the extension is prefixed by specor test.

For example, these tests files would be matched:

  • src/example.spec.js
  • src/__tests__/Login.jsx
  • src/__tests__/calculator.ts
  • src/another-example.test.js

But these wouldn't be matched:

  • src/Register.jsx
  • src/__tests__/style.css

Set up before each test

Jest has a key called setupFilesAfterEnv, which is nothing less than a list of files to run before each test runs. That's where you want to configure your testing frameworks (such as React Testing Library or Enzyme or create global mocks.

CRA, by default, named this file src/setupTests.js.

Configure test coverage

As said in the Jest CLI lesson, you can easily see your code coverage with the --coverage option. It's also possible to configure it.

Let's say you want (or don't want) specific files to be covered. You can use the collectCoverageFrom key for that. As an example, CRA wants code coverage on JavaScript or TypeScript files located in the src folder and don't want .d.ts (typings) files to be covered:

{
  "collectCoverageFrom": ["src/**/*.{js,jsx,ts,tsx}", "!src/**/*.d.ts"]
}
Enter fullscreen mode Exit fullscreen mode

If you want, you can also specify a coverage threshold thanks to the coverageThreshold key. In the following example, running jest --coverage will fail if there is less than 75% branch, line, function, and statement coverage:

{
  "coverageThreshold": {
    "global": {
      "branches": 75,
      "functions": 75,
      "lines": 75,
      "statements": 75
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Transform

If you use the very latest features of JavaScript or TypeScript, Jest may not be able to properly run your files. In this case, you need to transform them before they are actually run. For that, you can use the transform key, which maps regular expressions to transformers paths. As an illustration, CRA makes use of babel-jest for JS/TS files:

{
  "transform": {
    "^.+\\.(js|jsx|ts|tsx)$": "babel-jest"
  }
}
Enter fullscreen mode Exit fullscreen mode

As said in the beginning, there are a lot more configuration options for Jest. Be curious and take a look at their docs!

Top comments (3)

Collapse
 
samuelabreu profile image
Samuel Abreu

Useful method: fit, it skips all others tests on a file, but a dangerous one, found out yesterday a wrong commit skipping all other tests for weeks without no one noticing.

Good vscode extension: marketplace.visualstudio.com/items...

Collapse
 
thomaslombart profile image
Thomas Lombart • Edited

Good one!
To prevent these kind of things, you can take a loot at eslint-plugin-jest, especially this rule. It disallows focused tests (the use of .only or fit).

Collapse
 
petermortensen profile image
Peter Mortensen

I am missing some context. Is this only for JavaScript running on the server side (Node.js)?

What about client-side JavaScript (web browser), accessing the DOM, etc.?