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:
- Avoid repetition using
test.each
. - Skip tests with
test.skip
. - Focus on tests with
test.only
. - Mock functions.
- Testing async functions.
- Fake timers.
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 jest
has 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.