Testing
Tools
Jest
- Jest is a JavaScript testing framework
- Created by Facebook
Features:
- task runner
- assertion library
- mocking support
- snapshots
- isolated, paraellelized tests to maximize performance
Tools with similar scope include:
- Jasmine a JavaScript testing framework
- JUnit a Java testing framework
- NUnit a .NET testing framework
React Testing Library
React Testing Library is a library for testing React Components
Resembles the way the components are used by end users
It works more directly with DOM nodes, and therefore it's recommended to use with jest-dom for improved assertions.
Created by Kent C. Dodds
React Testing Library and Enzyme are alternatives for doing the same thing.
Its primary guiding principle is:
The more your tests resemble the way your software is used, the more confidence they can give you.
I recommend React Testing Library because of it's focus on not testing code implementation details
Features:
- Work with actual DOM nodes.
- The utilities this library provides facilitate querying the DOM in the same way the user would.
- Finding form elements by their label text (just like a user would)
- Finding links and buttons from their text (like a user would).
- Encourages your applications to be more accessible
Enzyme
Enzyme is a library for testing React Components
Provides testing utilities for React
Created by Airbnb
Enzyme uses the React Test Utilities (from the React team at Facebook) underneath, but is more convenient, readable, and powerful.
Enzyme and React Testing Library are alternatives for doing the same thing.
Features:
- renders a react component into a document
- shallow component rendering
- doesn't render child components
- full (mount) component rendering
- renders children: useful when
- components interact with DOM API
- need to test component lifecycle
- renders children: useful when
- query the DOM
- similar to jQuery
- simulates events
Jest DOM
- Custom jest matchers to test the state of the DOM
- Examples:
toHaveTextContent
,toHaveValue
,toHaveClass
ReactTestUtils
- Is part of
react
more specificallyreact-dom
import ReactTestUtils from 'react-dom/test-utils';
- It is the low level library that Enzyme uses internally
JSDOM
- JavaScript implementation of the DOM (Document object model)
- JavaScript based headless browser that can be used to create a realistic testing environment
- Used by default by React Testing Library
- While Jest provides browser globals such as window thanks to jsdom, they are only approximations of the real browser behavior.
- Jest is intended to be used for unit tests of your logic and your components rather than the DOM quirks.
Syntax
tests: test() or it()
test('test name', () => {
//implement test
});suites: describe()
describe('suite name', () => {
test('test1 name', () => {
//implement test
});
test('test2 name', () => {
//implement test
});
});assertions: expect().toBe()...
test('test name', () => {
expect(sum(1, 1)).toEqual(2);
});
Common question: What is the difference between
toBe
andtoEqual
?
toBe()
is shallow equalitytoEqual()
is deep equality
spies/mock functions: jest.fn()
JavaScript Tests
- Open the
my-app
project from the ProjectSetup demo. - Create the file
src\math.js
. - Add the following code
src\math.js
export function add(a, b) {
return a + b;
}
Create the file
math.test.js
Add the following code
src\math.test.js
import { add } from './math';
test('add numbers', () => {
expect(add(1, 1)).toEqual(2);
expect(add(2, 2)).toEqual(4);
});
Be sure to include the
import
statement.
- Open a command prompt or terminal in the
my-app
directory and run the tests
npm test
- The test should pass.
PASS src/math.test.js
- Type
w
to show more commands.
› Press f to run only failed tests.
› Press o to only run tests related to changed files.
› Press q to quit watch mode.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press Enter to trigger a test run.
- Try out the various commands
Mocking
Mocking Modules
If we want to mock out the math
module we could do the following:
- Create the directory
src\__mocks__
. - Create the file
src\__mocks__\math.js
. - Create the mock math module.
src\__mocks__\math.js
export function add(a, b) {
return 2;
}
- Mock the actual implementation and return the expected values.
rc/math.test.js
import { add } from './math';
+ jest.mock('./math');
test('add numbers', () => {
expect(add(1, 1)).toEqual(2);
expect(add(2, 2)).toEqual(4);
});
Notice we are able to return values regardless of the inputs because we are mocking the module.
console output
Expected: 4
Received: 2
3 | test('should add numbers', () => {
4 | expect(add(1, 1)).toEqual(2);
> 5 | expect(add(2, 2)).toEqual(4);
| ^
6 | });
7 |
at Object.<anonymous> (src/math.test.js:5:21)
Test Suites: 1 failed, 1 passed, 2 total
Jest works based on the convention that if you create the mock module in a
__mocks__
directory next to the actual module and then calljest.mock(./my-module)
the actual implementation will be replaced with your mock code.
But we can't meet both of our original expectations 2
and 4
because the mock is hard-coded to return the value 2
.
Mocking Functions
To solve this we can mock not only the module but the add
function as follows:
Replace the
add
function with a Jest mock function.src\__mocks__\math.js
export const add = jest.fn();
- export function add(a, b) {
- return 2;
- }Mock the return values.
src\math.test.js
import { add } from './math';
jest.mock('./math');
test('add numbers', () => {
+ add.mockReturnValueOnce(2);
+ add.mockReturnValueOnce(4);
expect(add(1, 1)).toEqual(2);
expect(add(2, 2)).toEqual(4);
});Now both expectations pass.
PASS src/math.test.js
With our current implementation if we call the add
function a third time we get undefined
because no default
implementation is defined.
Add another assertion.
src\math.test.js
import { add } from './math';
jest.mock('./math');
test('add numbers', () => {
add.mockReturnValueOnce(2);
add.mockReturnValueOnce(4);
expect(add(1, 1)).toEqual(2);
expect(add(2, 2)).toEqual(4);
+ expect(add(1,1)).toEqual(2)
});Verify the test fails.
FAIL src/math.test.js
● add numbers
expect(received).toEqual(expected) // deep equality
Expected: 2
Received: undefined
6 | expect(add(1, 1)).toEqual(2);
7 | expect(add(2, 2)).toEqual(4);
> 8 | expect(add(1,1)).toEqual(2)
| ^
9 | });
at Object.toEqual (src/math.test.js:8:20)Define a default implementation for our mock function. Below we define a default implementation to always return
42
.src\math.test.js
import { add } from './math';
jest.mock('./math');
test('add numbers', () => {
add.mockReturnValueOnce(2;
add.mockReturnValueOnce(4);
+ add.mockReturnValue(42);
expect(add(1, 1)).toEqual(2);
expect(add(2, 2)).toEqual(4);
+ expect(add(2, 2)).toEqual(42);
+ expect(add(1, 1)).toEqual(42);
});Verify the default implementation works on subsequent calls.
PASS src/math.test.js
To learn more about mock functions vistit the official documentation.
Debugging Tests
Debugging Tests in Chrome
Add the following to the
scripts
section in your project'spackage.json
package.json
"scripts": {
"test:debug": "react-scripts --inspect-brk test --runInBand --no-cache"
}Place
debugger;
statements in your test.math.test.js
test('add numbers', () => {
+ debugger;
expect(add(1, 1)).toEqual(2);
expect(add(2, 2)).toEqual(4);
});Run the command.
npm run test:debug
This will start running your Jest tests, but pause before executing to allow a debugger to attach to the process.
Open the following in Chrome
about:inspect
Choose the inspect link next to the process you want to debug.
After opening that link, the Chrome Developer Tools will be displayed.
Select
inspect
on your process and a breakpoint will be set at the first line of the react script (this is done simply to give you time to open the developer tools and to prevent Jest from executing before you have time to do so).Be patient waiting for the breakpoint to be hit it takes awhile.
Click the button that looks like a "play" button in the upper right hand side of the screen to continue execution. When Jest executes the test that contains the debugger statement, execution will pause and you can examine the current scope and call stack.
Note: the --runInBand cli option makes sure Jest runs test in the same process rather than spawning processes for individual tests. Normally Jest parallelizes test runs across processes but it is hard to debug many processes at the same time.
Debugging Tests in Visual Studio Code
Debugging Jest tests is supported out of the box for Visual Studio Code.
Use the following launch.json
configuration file:
Click
Debug > Start Debugging > Choose 'Node'
as the configuration type and add the configuration below.{
"version": "0.2.0",
"configurations": [
{
"name": "Debug CRA Tests",
"type": "node",
"request": "launch",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/react-scripts",
"args": ["test", "--runInBand", "--no-cache", "--watchAll=false"],
"cwd": "${workspaceRoot}",
"protocol": "inspector",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}
Running One Test
If your test name is unique, you can enter t
while in watch mode and enter the name of the test you'd like to run.
Excluding Tests
You can replace it()
with xit()
(or test()
with xtest()
) to temporarily exclude a test from being executed.
Component Tests (with React Testing Library)
React Testing Library is now installed by default in Create React App projects but if you have a project created before this change or not using Create React App then you follow these steps to install it.
Install React Testing Library
npm
npm install --save @testing-library/react @testing-library/jest-dom
Alternatively you may use
yarn
:Yarn
yarn add @testing-library/react @testing-library/jest-dom
Create the file
src/setupTests.js
(if it doesn't already exist)Add the following code:
src/setupTests.js
// this adds jest-dom's custom assertions
import '@testing-library/jest-dom/extend-expect';Review the generated test to see how.
src/App.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
React Testing Library Tips
- Use screen.debug() to see the HTML markup you are trying to select using various queries.
- Paste the HTML output of
screen.debug()
into the left panel on the testing-playground.com to see which queries you should use to access a given element. - If the query supplied is not good consider that the markup in your app is not accessible and endevour to learn more about accessibility.
Even if you don't care about accessibility your tests will become more reliable and less brittle by embracing it.
- Prefer ByRole
- More details on which query to use can be found here.
Note: Be sure to click on the button not the text of the button to get the correct query ByRole on the testing-playground.com.
- Understand the difference between
find
versusget
methods.find
awaits,get
doesn't- Queries
- async await
- React Testing Library does not encourage you to mock child components so often you are writing integration tests instead of unit tests.
- Test components that use APIs with
msw
- Which query to use for React Testing Library?
- Accessibility Cheat Sheet for, id role button, role form requires name, role alert
Components Tests (with Enzyme)
Different ways to test:
- smoke
- shallow
- full
Smoke
A smoke test
just verifies that component renders without throwing.
Shallow
Shallow testing tests a component in isolation from the child components it renders.
This requires the shallow()
rendering API from Enzyme.
To install it, run:
npm install --save enzyme enzyme-adapter-react-16 react-test-renderer
Alternatively you may use
yarn
:yarn add enzyme enzyme-adapter-react-16 react-test-renderer
Also install the types:
npm install @types/enzyme @types/react-test-renderer --save-dev
As of Enzyme 3, you will need to install Enzyme along with an Adapter corresponding to the version of React you are using. (The examples above use the adapter for React 16.)
The adapter will also need to be configured in your [global setup file]:
Create the file
src/setupTests.js
Add the following code:
// src/setupTests.js
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });Stop
npm test
usingCtrl+C
Run the command
npm test
so it configures the adapterAdd the following test
// src/App.test.js
...
//shallow
import { shallow } from 'enzyme';
test('shallow renders without crashing', () => {
shallow(<App />);
});You should see an additional test passing.
Component Snapshot Tests (with Jest)
Snapshot testing is a feature of Jest that automatically generates text snapshots of your components and saves them on the disk so if the UI output changes, you get notified without manually writing any assertions on the component output. Read more about snapshot testing.
.. avoid huge snapshots and take smaller, more focused ones. While you're at it, see if you can actually change it from a snapshot to a more explicit assertion (because you probably can 😉). - Kent C Dodds
Snapshot testing requires you to install the react-test-renderer
which you may have already done for the Enzyme shallow testing section but if you skipped it you will need to install it now.
The react-test-renderer
library allows you to render React components as JavaScript objects without using the DOM which improves performance of these tests.
If you haven't previously install the
react-test-renderer
.npm install react-test-renderer
Add the following test
src\App.test.[js|tsx]
//snapshot
import renderer from 'react-test-renderer';
test('has a valid snapshot', () => {
const component = renderer.create(<App />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});Normally you wouldn't snapshot the
App
component since it contains all other componentsOpen the file
src/__snapshots__\App.test.[js|tsx].snap
Update
src/App.js
.src/App.js
function App() {
return (
<div className="App">
<header className="App-header">
+ <h1>Welcome to React</h1>
<img src={logo} className="App-logo" alt="logo" />See the message below.
› 1 snapshot failed.
Snapshot Summary
› 1 snapshot failed from 1 test suite. Inspect your code changes or press `u` to update them.Press
u
to update the snapshot.
Reference
General
Jest
- Jest: JavaScript Testing Framework
- Jest Mocking & Spies
- Handling Async in Jest
- What is the difference between jest.fn() and jest.spyOn() methods in jest?
- Jest Matchers
- Jest DOM: Jest Matchers for the DOM
- Testing React Apps (with Jest)
- Effective Snapshot Testing