Skip to content

Testing with Jest (tips and tricks)

This post covers some tips and tricks while testing JavaScript with Jest. We’ll start with some configuration and setup, followed by some testing practices:

Initial setup

Here are some basic steps to setup jest .

Create a project folder and initialize npm.

$ mkdir jest-testing && cd jest-jesting
$ npm init -y

Install the necessary dependencies.

$ npm install jest babel-jest regenerator-runtime

babel-jest will be used by Jest to transform modern JavaScript code like import statements and arrow functions () => {}.

regenerator-runtime is necessary for async functions.

After installing those packages, create the files babel.config.js and jest.config.js at the root directory.

// babel.config.js
module.exports = {presets: ['@babel/preset-env']}
// jest.config.js
module.exports = {
  transform: {
    "^.+\\.js$": "babel-jest",
  }
};

With this setup, we’re instructing jest to transform .js files using babel-jest.

Finally, let’s add the following script to package.json:

{
...
"scripts": {
    ...
    "test": "jest"
  },
}

This way, we’ll be able to trigger the test suites with npm test.

Project structure

Throughout this tutorial we’ll “test” the following dummy module: add.js located at folder src.

// add.js
export const add = (a, b) => a + b;

Our initial tests file will be add.test.js:

// add.test.js
test("testing", () => {});

The resulting folder structure of our project will be as follows:

.
├── babel.config.js
├── jest.config.js
├── package.json
├── package-lock.json
├── setupTests.js
└── src
    ├── add.js
    └── add.test.js

The tests can be triggered with npm run test or with npx jest. jest will automatically pick up all .test.js files and execute the tests in them.

Alternatively, we could place the tests in a __tests__ folder. The best practice is to place either the .test.js files or the __tests__ folder right next to the tested file.

Configuring jest

There are many different options that can be passed to the jest.config.js file. Let’s see a few of them.

roots is used to define the paths that jest should look test files in. In our case, that’s only the src directory.

roots: ["<rootDir>/src/"],

<rootDir> is the path where the jest.config.js file is located.

setupFilesAfterEnv defines files that should be executed just before running the actual tests. Our setup file will be setupTests.js located at the root directory.

setupFilesAfterEnv: ['./setupTests.js'],

For example, we need to import the regenerator runtime for async functions to work on our tests. Instead of importing it explicitly on every test file, we can do so at the tests setup file.

// setupTests.js
import "regenerator-runtime/runtime";

Finally, the extraGlobals allows us to specify global methods that will be used in the tests for faster lookup. For example, our tests will require functions from the Math module.

extraGlobals: ["Math"]

Note that we can still call Math functions inside our tests without setting this property. It’s used only to make references to Math faster.

Our resulting jest.config.js file would be:

module.exports = {
  transform: {
    "^.+\\.js$": "babel-jest",
  },
  roots: ["<rootDir>/src/"],
  setupFilesAfterEnv: ["./setupTests.js"],
  extraGlobals: ["Math"],
};

Now that we have jest and our project configured, let’s start testing!

Avoid repetition with test.each

Consider the following two tests:

// add.test.js
import { add } from "./add";

test("add 1 and 1 equals 2", () => {
  const result = add(1, 1);
  const expected = 2;
  expect(result).toBe(expected);
});

test("add 2 and 2 equals 4", () => {
  const result = add(2, 2);
  const expected = 4;
  expect(result).toBe(expected);
});

We can see that besides changing some values, the second test is almost an exact copy of the first test. This is a good use case for test.each

test.each([
  [1, 1, 2],
  [2, 2, 4],
])("add %d + %d equals %d", (val1, val2, expectedVal) => {
  const result = add(val1, val2);
  expect(result).toBe(expectedVal);
});

The first argument to test.each is a 2D array. Each row in the array contains the arguments that will be passed to the function that defines the test. See that not only the test function is parameterized, also the test description can be parameterized as a printf string.

In the previous example, each of %d is a placeholder for an integer value. The first %d takes the first numerical value in the array and so on.

Skipping tests with test.skip

Let’s say that, for some reason, we want to keep the first two tests we defined above. They could be useful for documentation or we might need them later on. Instead of commenting them to avoid running the tests each time, we can, use test.skip.

test.skip("add 1 and 1 equals 2", () => ...);

test.skip("add 2 and 2 equals 4", () => ...);

jest will ignore these tests when running npm test.

We can even use it with test.each:

test.skip.each(...)(...);

When a test fails, try using test.only

test.only is helpful in two situations. First, imagine you have a test suite with several tests. You add a new test and you would like to verify that this single new test passes. Adding .only to the test will make jest execute only that single test (and any other test.only tests).

In the example below, only test n would be executed.

describe("My test suite", () => {
  test("1", ...);
  test("2", ...);
  ...
  test.only("n", ...);
});

The second use case is when we have a test that fails. Before any further inspection, it’s helpful to verify if the test passes when we execute only it. If the test passes as test.only, that means that the problem is not in the test itself, but may be due to a side-effect in other tests. We’ll see such an example next.

Be careful when mocking functions and side-effects

Let’s define the following function in add.js that calls Math.abs under the hood:

export const addAbs = (a, b) => Math.abs(a) + Math.abs(b);

In general, we want to limit our testing only to our own implementation. We don’t want to test external modules or functions. Since addAbs calls Math.abs, we’ll start testing addAbs using a mock implementation of Math.abs.

// add.test.js
import { addAbs } from "./add";

test("addAbs 1, -1 equals 2 (with mocking)", () => {
  Math.abs = jest.fn((x) => 1);
  const result = addAbs(Math.abs(1), Math.abs(-1));
  expect(result).toBe(2);
});

This mock implementation fulfills the needs of our test because we know the absolute value of both -1 and 1 is 1.

Now that we’re more confident with our implementation, let’s make another test. This time, without mocking Math.abs.

test("addAbs 0, 0 equals 0 (without mocking)", () => {
  const result = addAbs(0, 0);
  const expected = 0;
  expect(result).toBe(expected);
});

Surprisingly, our second test will fail!

 FAIL  src/add.test.js
  ✓ addAbs 1, -1 equals 2 (with mocking) (2 ms)
  ✕ addAbs 0, 0 equals 0 (without mocking) (2 ms)

  ● addAbs 0, 0 equals 0 (without mocking)

    expect(received).toBe(expected) // Object.is equality

    Expected: 0
    Received: 2

By inspecting the received result during the second test, we can conclude that Math.abs is still calling the mock function. The line:

 Math.abs = jest.fn((x) => 1);

Has a global effect!

Let’s see some solutions for this problem.

Change the order of tests

We can simply move the mock function tests after the tests that don’t mock functions.

test("addAbs 0, 0 equals 0 (without mocking)", () => {
  const result = addAbs(0, 0);
  const expected = 0;
  expect(result).toBe(expected);
});

test("addAbs 1, -1 equals 2 (with mocking)", () => {
  Math.abs = jest.fn((x) => 1);
  const result = addAbs(Math.abs(1), Math.abs(-1));
  const expected = 2;
  expect(result).toBe(expected);
});

Both tests will pass now.

Manually restoring the mocked function

The second option is to save the reference of the original function and restore it before leaving the test.

test("addAbs 1, -1 equals 2 (with mocking and restoring)", () => {
  const previousFunc = Math.abs;
  Math.abs = jest.fn((x) => 1);
  const result = addAbs(1, -1);
  const expected = 2;
  expect(result).toBe(2);
  Math.abs = previousFunc;
});

test("addAbs 0, 0 equals 0 (without mocking)", () => {
  const result = addAbs(0, 0);
  const expected = 0;
  expect(result).toBe(expected);
});

Now, both tests will pass regardless of their order.

Restore references with jest.restoreAllMocks

Functions like jest.restoreAllMocks and mockRestore, restore the original function reference. However, these functions work only for mock functions created with jest.spyOn.

test("addAbs 1, -1 equals2 (mock with jest.spyOn)", () => {
  Math.abs = jest.spyOn(Math, "abs").mockImplementation((x) => 1);
  const result = addAbs(1, -1);
  const expected = 2;
  expect(result).toBe(expected);
  Math.abs.mockRestore();
  // or
  // jest.restoreAllMocks();
});

See that after creating the mock function with jest.spyOn, we give it an implementation with mockImplementation. Just calling jest.spyOn does not modify the original function reference.

An alternative to this method is to set the restoreMocks option to true in jest.config.js to automatically restore mocks after each single test.

Async testing

Let’s add the function addAsync that returns the sum result only after a short delay.

// add.js
export const asyncAdd = (a, b) =>
  new Promise((resolve) =>
    setTimeout(() => {
      resolve(a + b);
    }, 500)
  );

Let’s test it as follows:

// add.test.js
import { addAsync } from "./add"

test("addAsync 1, 1 equals 2", () => {
  const result = addAsync(1, 1);
  const expected = 2;
  expect(result).toBe(expected);
});

This test will fail.

 FAIL  src/add.test.js
  ✕ addAsync 1, 1 equals 2 (5 ms)

  ● addAsync 1, 1 equals 2

    expect(received).toBe(expected) // Object.is equality

    Expected: 2
    Received: {}

jest does not wait for the promise to resolve. Moreover, we’re comparing a Promise object (the received value) with our expected result.

The simplest solution is to use the async/await syntax.

test("addAsync 1, 1 equals 2 (async/await)", async () => {
  const result = await slowaddAsync(1, 1);
  const expected = 2;
  expect(result).toBe(expected);
});

Another solution is to call resolves on the Promise.

test("addAsync 1, 1 equals 2 (resolves)", () => {
  const result = addAsync(1, 1);
  const expected = 2;
  return expect(result).resolves.toBe(expected);
});

Note that we have to return the resolves statement for jest to wait for the promise to resolve. (The test passed without adding return in this case, but jest docs recommend doing it)

The previous two tests will both pass.

Use fake timers to test delayed events

Finally, let’s add the following function. It’s a variation of addAsync that takes longer (10 seconds) to resolve.

// add.js
export const addAsyncSlow = (a, b) =>
  new Promise((resolve) =>
    setTimeout(() => {
      resolve(a + b);
    }, 10000)
  );

Let’s test it with async/await:

// add.test.js
import { addAsyncSlow } from "./add";

test("addAsyncSlow 1, 1 equals 2 (async/await)", async () => {
  const result = await addAsyncSlow(1, 1);
  const expected = 2;
  expect(result).toBe(expected);
});

This test will fail.

 FAIL  src/add.test.js (5.811 s)
  ✕ addAsyncSlow 1, 1 equals 2 (async/await) (5002 ms)
 
  ● addAsyncSlow 1, 1 equals 2 (async/await)

    thrown: "Exceeded timeout of 5000 ms for a test.
    Use jest.setTimeout(newTimeout) to increase the timeout value, if this is a long-running test."

We can see that jesthas a timeout of 5 seconds. Since addAsyncSlow takes 10 seconds to resolve, it fails.

While jest suggests using jest.setTimeout, we can do better using jest.useFakeTimers.

jest.useFakeTimers will use fake versions of the timer functions. This fake implementation can be manipulated to instantly resolve timers.

test("addAsyncSlow 1, 1 equals 2 (fake timers)", () => {
  jest.useFakeTimers();
  const result = addAsyncSlow(1, 1);
  const expected = 2;
  jest.runAllTimers();
  jest.useRealTimers();
  return expect(result).resolves.toBe(expected);
});

jest.runAllTimers will make all pending timers resolve immediately.

In order to avoid unexpected side-effects on other tests, we restore the real timers with jest.useRealTimers at the end of the test.

If we wanted to use fake timers on all tests, we could extract these statements with beforeEach and aferEach.

beforeEach(() => jest.useFakeTimers());
afterEach(() => jest.useRealTimers());

While this example is contrived, fake timers are helpful to test DOM events that take some time to complete. For example, when we wait a certain delay for user interaction.

Published inProgramming
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments