/

Testing JavaScript with Jest

Testing JavaScript with Jest

Jest is a library that allows you to test JavaScript code, and it is widely used for testing React applications. It is fast, easy to use, and maintained by Facebook. In this article, we will explore the different features of Jest and how to write effective tests using it.

Introduction to Jest

Jest is an open-source project maintained by Facebook. It is designed for testing JavaScript code and is particularly well-suited for testing React applications. However, it can be used to test any JavaScript code. Some of the strengths of Jest include:

  • It is fast
  • It supports snapshot testing
  • It provides an all-in-one solution without requiring additional testing libraries

Compared to other testing frameworks like Mocha, Jest is more opinionated and provides a set of conventions out of the box. This makes it easier to set up and start writing tests without having to make many configuration choices. Additionally, Jest has excellent tooling integrations and is widely adopted within the JavaScript community.

Installation

If you are using create-react-app, Jest is automatically installed and configured for you, so you don’t need to install it separately. Otherwise, you can install Jest using either Yarn or npm:

1
yarn add --dev jest

or

1
npm install --save-dev jest

Make sure to add the following line to the scripts section of your package.json file:

1
2
3
4
5
{
"scripts": {
"test": "jest"
}
}

This allows you to run your tests using the command yarn test or npm run test. Alternatively, you can install Jest globally by executing the following command:

1
yarn global add jest

This allows you to run all your tests using the jest command in the terminal.

Creating the first Jest test

If you are using create-react-app, Jest is already set up with a default test file. However, if you need to add Jest to an existing project, you can do so by following these steps:

  1. Create a file called math.js and define some functions that you want to test. For example:
1
2
3
4
5
6
const sum = (a, b) => a + b;
const mul = (a, b) => a * b;
const sub = (a, b) => a - b;
const div = (a, b) => a / b;

module.exports = { sum, mul, sub, div };
  1. Create a test file called math.test.js in the same folder. In this file, you can use Jest to test the functions defined in math.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const { sum, mul, sub, div } = require('./math');

test('Adding 1 + 1 equals 2', () => {
expect(sum(1, 1)).toBe(2);
});

test('Multiplying 1 * 1 equals 1', () => {
expect(mul(1, 1)).toBe(1);
});

test('Subtracting 1 - 1 equals 0', () => {
expect(sub(1, 1)).toBe(0);
});

test('Dividing 1 / 1 equals 1', () => {
expect(div(1, 1)).toBe(1);
});

To run your tests, execute the command yarn test or npm run test in your terminal. Jest will run all the test files it finds and display the results.

Running Jest with VS Code

If you are using Visual Studio Code as your editor, you can enhance your testing experience by installing the Jest extension (available in the marketplace). Once installed, the extension automatically detects if Jest is installed in your project and runs the tests. You can also manually invoke the tests by selecting the “Jest: Start Runner” command. This allows you to run the tests and stay in watch mode, which means the tests will be re-run whenever you make changes to the test files or the code being tested.

Matchers

Jest provides a variety of matchers that allow you to test values in different ways. Some commonly used matchers include:

  • toBe(): compares the strict equality of the expected value and the actual value
  • toEqual(): compares the values of two variables or objects
  • toBeNull(): checks if the value is null
  • toBeDefined(): checks if the value is defined
  • toBeUndefined(): checks if the value is undefined
  • toBeCloseTo(): compares floating-point numbers with a certain precision
  • toBeTruthy(): checks if the value is truthy
  • toBeFalsy(): checks if the value is falsy
  • toBeGreaterThan(): checks if the actual value is greater than the expected value
  • toBeGreaterThanOrEqual(): checks if the actual value is greater than or equal to the expected value
  • toBeLessThan(): checks if the actual value is less than the expected value
  • toBeLessThanOrEqual(): checks if the actual value is less than or equal to the expected value
  • toMatch(): checks if a string matches a regular expression pattern
  • toContain(): checks if an array or string contains the expected value
  • toHaveLength(): checks the length of an array or string
  • toHaveProperty(): checks if an object has a specific property and value
  • toThrow(): checks if a function throws an exception
  • toBeInstanceOf(): checks if an object is an instance of a specific class

All of these matchers can be negated using the .not modifier. For example, you can use expect().not.toEqual() to check for inequality.

Setup and Teardown

Before running your tests, you may need to perform some setup or cleanup tasks. Jest provides hooks for setting up and tearing down test environments.

You can use the beforeAll() and afterAll() functions to initialize or clean up the test environment once. These functions run only once, before or after all the tests:

1
2
3
4
5
6
7
beforeAll(() => {
// do something before all tests
});

afterAll(() => {
// do something after all tests
});

If you need to perform a setup or cleanup task before or after each test, you can use the beforeEach() and afterEach() functions:

1
2
3
4
5
6
7
beforeEach(() => {
// do something before each test
});

afterEach(() => {
// do something after each test
});

These functions can be useful for tasks like creating temporary files or initializing a test database. They ensure that each test is run in isolation and does not interfere with the state of other tests.

Grouping Tests using describe()

Jest allows you to group tests together using the describe() function. This function creates a test suite and helps with organizing and structuring your tests. You can nest describe() blocks to create hierarchical test structures:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
describe('First set of tests', () => {
beforeEach(() => {
// do something before each test in this describe block
});

afterAll(() => {
// do something after all tests in this describe block
});

test(/* ... */);
test(/* ... */);
});

describe('Second set of tests', () => {
beforeEach(() => {
// do something before each test in this describe block
});

beforeAll(() => {
// do something before all tests in this describe block
});

test(/* ... */);
test(/* ... */);
});

This allows you to organize your tests into logical groups and provides a clear structure to your test code.

Testing Asynchronous Code

Testing asynchronous code can be challenging, as the test needs to wait for the async operation to complete. Jest provides several approaches to test asynchronous code, depending on the use case.

Callbacks

If you are working with code that uses callbacks, you can pass a callback parameter to the test function and invoke it when the async operation is complete. Jest will wait until done() is called before finishing the test:

1
2
3
4
5
6
7
8
9
10
11
function fetchData(callback) {
// do something asynchronously
callback(result);
}

test('test with callback', (done) => {
fetchData((result) => {
expect(result).toBe(/* expected result */);
done();
});
});

Promises

For code that uses Promises, you can return a Promise from the test function. Jest will wait for the Promise to resolve or reject before finishing the test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function fetchData() {
return new Promise((resolve, reject) => {
// do something asynchronously
if (/* condition */) {
resolve(result);
} else {
reject(error);
}
});
}

test('test with Promise', () => {
return fetchData().then((result) => {
expect(result).toBe(/* expected result */);
});
});

Async/await

If you are using async/await, you can use the async keyword in the test function and await to wait for the Promise to resolve:

1
2
3
4
5
6
7
8
9
async function fetchData() {
// do something asynchronously
return result;
}

test('test with async/await', async () => {
const result = await fetchData();
expect(result).toBe(/* expected result */);
});

Jest will wait for the async operation to complete before finishing the test.

Mocking

Mocking allows you to replace parts of your code to isolate the behavior being tested. It is useful for testing functionality that depends on external systems, such as databases, networks, or files.

Jest provides built-in mocking capabilities that allow you to mock packages or individual functions.

Spy packages without affecting the functions code

You can use jest.spyOn() to spy on the execution of a function in a package without modifying its behavior:

1
2
3
4
5
6
7
8
9
const mathjs = require('mathjs');

test('Spied mathjs log function', () => {
const spy = jest.spyOn(mathjs, 'log');
const result = mathjs.log(10000, 10);

expect(mathjs.log).toHaveBeenCalled();
expect(mathjs.log).toHaveBeenCalledWith(10000, 10);
});

This allows you to track if a function has been called and check the parameters it was called with.

Mock an entire package

Jest provides a convenient way to mock entire packages. You can create a __mocks__ folder in the root of your project and create a JavaScript file for each package you want to mock. For example, if you want to mock mathjs, create a file called __mocks__/mathjs.js with the following content:

1
2
3
module.exports = {
log: jest.fn(() => 'mocked result'),
};

Then, in your test file, import mathjs as usual, and the mock implementation will be used instead:

1
2
3
4
5
6
7
8
9
const mathjs = require('mathjs');

test('Mocked mathjs log function', () => {
const result = mathjs.log(10000, 10);

expect(result).toBe('mocked result');
expect(mathjs.log).toHaveBeenCalled();
expect(mathjs.log).toHaveBeenCalledWith(10000, 10);
});

Mock a single function

If you only need to mock a single function, you can use jest.fn():

1
2
3
4
5
6
7
8
9
10
11
const mathjs = require('mathjs');

mathjs.log = jest.fn(() => 'mocked result');

test('Mocked mathjs log function', () => {
const result = mathjs.log(10000, 10);

expect(result).toBe('mocked result');
expect(mathjs.log).toHaveBeenCalled();
expect(mathjs.log).toHaveBeenCalledWith(10000, 10);
});

This replaces the original implementation of mathjs.log with the mock implementation, allowing you to control the return value and track calls to the function.

Pre-built mocks

There are also pre-built mocks available for popular libraries that provide additional functionalities. For example, the jest-fetch-mock package allows you to mock fetch() calls and specify sample return values without interacting with the actual server in your tests.

Snapshot Testing

Snapshot testing is a powerful feature provided by Jest. It allows you to capture the output of a component or function and compare it against a previously saved snapshot. If the output has changed, Jest will highlight the difference and allow you to update the snapshot if the change is expected.

To use snapshot testing, you need to install react-test-renderer (if you haven’t already) and write a test that renders your component or function and compares it to the saved snapshot:

1
2
3
4
5
6
7
8
import React from 'react';
import MyComponent from './MyComponent';
import renderer from 'react-test-renderer';

test('renders correctly', () => {
const tree = renderer.create(<MyComponent />).toJSON();
expect(tree).toMatchSnapshot();
});

The first time you run this test, Jest will save the snapshot to a __snapshots__ folder. On subsequent test runs, Jest will compare the output to the saved snapshot. If there is a difference, Jest will display an error and give you the option to update the snapshot by pressing u in the terminal.

Snapshot testing is particularly useful for UI components and can help you catch unintended changes to the UI. However, it should not be used as the sole method of testing complex logic or data transformations.

Conclusion

Jest is a powerful and easy-to-use testing framework for JavaScript that is especially well-suited for testing React applications. In this article, we explored the different features of Jest, including installation, writing tests, using matchers, setting up and tearing down test environments, mocking, and snapshot testing. By leveraging the features provided by Jest, you can write comprehensive tests that help ensure the quality and reliability of your JavaScript code.