React Test

Let’s learn some basics on how to use Jest and React Testing Library to test React components.

Jest Basic

Jest is a delightful JavaScript Testing Framework with a focus on simplicity.

Application created using Create React App already contains Jest, so you don’t need to install it separately.

To run the test, just run npm test command. all the files with .test.js or .spec.js extension will be executed.

The simpliest Jest test:

1
2
3
4
// sum.test.js
test('adds 1 + 2 to equals 3', () => {
expect(1+2).toBe(3);
})

Matchers

toBe is a matcher. toBe uses Object.is to test exact equality. If you want to check the value of an object, use toEqual instead. toEqual recursively checks every field of an object or array.

1
2
3
4
5
test("object assignment", () => {
const data = { one: 1 };
data["two"] = 2;
expect(data).toEqual({ one: 1, two: 2 });
});

There are other matches that tests numbers, strings, arrays exceptions. see Using Matchers for more details. For the full list, see expect API

Testing Async Code

Callback

By default, Jest tests complete once they reach the end of their execution, so the following test will not work. The callback method is never called.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Don't do this
function fetchData(cb) {
setTimeout(() => {
const data = true
cb(data)
})
}

test("return value is true", () => {
function callback(data) {
console.log("inside callback")
expect(data).toBeTruthy();
}
fetchData(callback)
})

To fix this, pass a done argument to the test. Jest will wait until the done callback is called before finishing the test.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Do this
function fetchData(cb) {
setTimeout(() => {
const data = true
cb(data)
})
}

test("return value is true", done => {
function callback(data) {
expect(data).toBeTruthy();
done();
}
fetchData(callback)
})

Promise

Promise - it is more straightforward to test promise. When you return a promise from Jest test case, Jest will wait for that prmise to resolve.

1
2
3
4
5
6
7
8
9
10
11
12
13
function myPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('foo')
}, 1000)
})
}

test("promise return foo", () => {
return myPromise().then(data => {
expect(data).toBe('foo')
})
})

To test a promise that rejects

1
2
3
4
5
6
7
8
9
10
11
12
13
function myRejectPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('foo')
}, 1000)
})
}

test("test promise that rejects", () => {
return myRejectPromise().catch(data => {
expect(data).toBe('foo')
})
})

Async and Await

An alternative is to use async and await to test Promise.

1
2
3
4
5
6
7
8
9
10
11
12
function myPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('foo')
}, 1000)
})
}

test("test promise that rejects", async () => {
let returnedValue = await myPromise();
expect(returnedValue).toBe('foo')
})

Setup and Teardown

Jest provide `beforeEach, afterEach, beforeAll and afterAll methods for setup and teardown

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
beforeEach(() => {
console.log("beforeEach")
});

afterEach(() => {
console.log("afterEach")
});

beforeAll(() => {
console.log("beforeAll")
});

afterAll(() => {
console.log("afterAll")
});

By default, the before and after blocks apply to every test in a file. You can also group tests together using a describe block. When they are inside a describe block, the before and after blocks only apply to the tests within that describe block.

Mock Functions

Mock functions allow you to test the links between code by erasing the actual implementation of a function

Lets test forEach function

1
2
3
4
5
function forEach(items, callback) {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
}

we can use a mock callback function and inspect the mock’s state to ensure the callback is invoked as expected.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
test('test forEach', () => {
const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback);

// The mock function is called twice
expect(mockCallback.mock.calls.length).toBe(2);

// The first argument of the first call to the function was 0
expect(mockCallback.mock.calls[0][0]).toBe(0);

// The first argument of the second call to the function was 1
expect(mockCallback.mock.calls[1][0]).toBe(1);

// The return value of the first call to the function was 42
expect(mockCallback.mock.results[0].value).toBe(42);
})

Mock Return Values

The above mock callback function does not return a value. You can create mock functions that returns value.

1
2
3
4
5
6
7
8
const myMock = jest.fn();
console.log(myMock());
// > undefined

myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);

console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true

Mock Implementation

You can not only mock the return value but the implementation

1
2
3
4
const myMockFn = jest.fn(cb => cb(null, true));

myMockFn((err, val) => console.log(val));
// > true

Mock Modules

users.js - this class uses axis module

1
2
3
4
5
6
7
8
9
import axios from 'axios';

class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}

export default Users;

You can use jest.mock(...) function to automatically mock the axios module.

users.test.js

1
2
3
4
5
6
7
8
9
10
11
12
import axios from 'axios'
import Users from './users'

jest.mock('axios')

test('should fetch users', () => {
const users = [{uname: 'Bob'}]
const resp = {data: users}
axios.get.mockResolvedValue(resp)

return Users.all().then(data => expect(data).toEqual(users))
})

Testing React App

Create React App includes React Testing Library by default. React Testing Library is a simple and complete React DOM testing utilities that encourage good testing practices.

Here is a modified version of Basic Example from https://github.com/testing-library/react-testing-library

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// hidden-message.js
import React from 'react'

function HiddenMessage({children}) {
const [showMessage, setShowMessage] = React.useState(false)
return (
<div>
<label htmlFor="toggle">Show Message</label>
<input
id="toggle"
type="checkbox"
onChange={e => setShowMessage(e.target.checked)}
checked={showMessage}
/>
{showMessage ? children : null}
</div>
)
}

export default HiddenMessage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// hidden-message.test.js
import '@testing-library/jest-dom'
// NOTE: jest-dom adds handy assertions to Jest and is recommended, but not required

import React from 'react'
import {render, fireEvent} from '@testing-library/react'
import HiddenMessage from './hidden-message'

test('shows the children when the checkbox is checked', () => {
const testMessage = 'Test Message'
const {queryByText, getByLabelText, getByText} = render(<HiddenMessage>{testMessage}</HiddenMessage>)

// query* functions will return the element or null if it cannot be found
// get* functions will return the element or throw an error if it cannot be found
expect(queryByText(testMessage)).toBeNull()

// the queries can accept a regex to make your selectors more resilient to content tweaks and changes.
fireEvent.click(getByLabelText(/show/i))

// .toBeInTheDocument() is an assertion that comes from jest-dom
// otherwise you could use .toBeDefined()
expect(getByText(testMessage)).toBeInTheDocument()
})

To learn more details on using React Testing Library, see Video Tutorial: Intro to React Testing

Reference