RETROSPECTIVE

February 5th, 2020

Unit, Integration, and Snapshot Testing in React with Jest and Enzyme

Jest

Enzyme

React

JavaScript

NPM

Unit Test

Integration Test

Snapshot Test

In an article last fall I spoke about my change in mindset towards unit testing. In my early programming days I thought unit tests slowed down development to a fault. Nowadays they are mandatory in all my applications. Unit tests are assertions that a unit of code is working as expected in isolation of other components in the codebase. They promote code review, help catch recurring bugs, and ease the burden of upgrading and switching technologies.

For React applications, the de facto testing framework is Jest. Jest was written by Facebook with React unit testing in mind, making it a natural fit. Alongside Jest, Enzyme is used to test React components. Enzyme was created by AirBnB to render components for testing. It provides an API to traverse the rendered DOM nodes in a component and hook into stateful component instances.

My previous article consisted of a demo application which showcasing the new features in React 16.3 with detailed descriptions and sample components. The demo is a React application, so naturally I created a testing suite with Jest and Enzyme.

This article walks through the testing suite, providing insights about Jest and Enzyme in the process.

Any npm project begins with a package.json file and application dependencies. For the sake of brevity I only listed the dependencies needed for unit testing.

{ ... "dependencies": { ... "react": "16.12.0", "react-dom": "16.12.0", "react-router-dom": "^5.1.2" ... }, "devDependencies": { ... "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.1", "jest": "^24.9.0", "react-test-renderer": "^16.12.0", ... }, ... }

React is a standard dependency while Jest and Enzyme are dev dependencies. As its name suggests, dev dependencies are used for development purposes only. When a repository is cloned and npm install or yarn is invoked, both standard and dev dependencies are installed. However, when a module is installed with npm install {module_name} or yarn add {module_name}, only standard dependencies are installed. We don't want unit testing frameworks to be installed for module end-users, so their dependency definitions always exist in devDependencies.

The jest and enzyme dev dependencies are the Jest testing framework and Enzyme testing utility, respectively. enzyme-adapter-react-16 is used to configure Enzyme to work with React 16 code. There are many Enzyme adapters, ranging from React versions 0.13 to 161. I will configure the React 16 adapter soon.

react-test-renderer renders React elements and components as JavaScript objects without needing the DOM2. It's used for component snapshot testing.

With package.json initialized, it's time to create the applications Jest configuration. The Jest configuration is located alongside package.json in jest.config.js.

module.exports = { displayName: 'components', testEnvironment: 'jsdom', testMatch: ['**/test/**/*.test.js'], setupFilesAfterEnv: ['<rootDir>/test/setupTests.js'], maxConcurrency: 5, moduleNameMapper: { '\\.(png)$': '../../test/mocks/fileMock.js' }, transform: { '^.+\\.js$': 'babel-jest' }, collectCoverage: true, collectCoverageFrom: ['src/**/*.js'], coveragePathIgnorePatterns: ['src/index.js'], coverageThreshold: { 'global': { 'branches': 100, 'functions': 100, 'lines': 100, 'statements': 100 } } };

jest.config.js configures the testing suite for the application. Let's highlight some important aspects of the configuration. Tests are executed in the jsdom environment, a headless browser which provides rendered components access to the DOM and DOM API. Any file in the test directory (or its subdirectories) with the .test.js file extension is executed in the test suite. The Enzyme adapter is configured in the setupTests.js file, which has a very simple body:

import {configure} from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; configure({ adapter: new Adapter() });

Before testing, all Javascript files are transpiled with Babel and non-JavaScript imports are mocked with a file named fileMock.js. This file contains a single line:

module.exports = 'test-file-stub';

All that remains in the Jest configuration file is code coverage setup. All source code JavaScript files are tested for code coverage (except for index.js which consists of React and React Router bootstrapping). In order for the test suite to pass, code coverage must be 100%.

Code Coverage

Code coverage is the percentage of a program that executes in a test suite. Code coverage is usually run alongside tests, and generates a text or html report. While 100% code coverage is ideal, it isn't always practical or necessary. Likewise, 100% code coverage doesn't mean that code is properly tested. Poorly written tests count towards code coverage the same as properly written ones!

At this point the test suite is fully configured! In my experience Jest configuration is pretty easy to work with. Without much work, I created a test suite that is executable with the jest command. I often alias this command in my package.json.

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

I wrote three types of tests for my React 16.3 demo - unit tests, integration tests, and snapshots.

Testing Methods

Unit Tests

A unit test is an assertion that a unit of code is working as expected. Units of code can range from a single line or function to an entire class or application. Unit tests are often run in isolation, without impact from external dependencies. In React, units are often methods or shallow rendered components (components rendered without their child components).

Integration Tests

In an integration test, units of code are combined and tested together as a group. The grouping of code can range from two functions to an entire webpage or application. In React, this grouping is often a module or a component rendered on the DOM with its child components.

Snapshot Tests

A snapshot test takes a snapshot of the UI and saves it. On subsequent snapshot tests, the saved snapshot is compared to the current UI. If the UI changed since the snapshot was taken, the test fails. Snapshot tests are very helpful for preventing unwanted UI changes3. In React, snapshots are created for components.

Unit tests assert that a unit of code behaves as expected. When writing unit tests, we use three main functions provided by Jest - describe(), it(), and assert(). These functions aren't explicitly imported into test code, Jest injects them implicitly when the tests execute.

describe() creates a grouping of test cases (test suite) and it() creates a single test case. Test cases pass if all their assertions (created with assert() and a corresponding matcher) are true. Test cases fail as soon as a single assertion fails. Matchers are chained onto assert() global functions. Some examples of matchers are toEqual(), toBeTruthy(), and toContain().

A common "Hello World" unit test example adds two values together and asserts the result is a third value. I created this "Hello World" test application, which is easily executed with npm install and npm run test. Here is the Jest code:

describe('unit tests', () => { it('adds two numbers', () => { expect(1 + 1).toEqual(2); }); });

This code creates a single test case named "adds two numbers" in a single test suite named "unit test." It also successfully proves that 1 + 1 = 2. Running this code with npm run test produces the following output:

The next piece needed for React unit tests is Enzyme. I previously mentioned that React component unit tests are implemented with shallow rendering. Shallow rendering only renders the React elements in a components render method (or return value for functional components), excluding any child components. Enzyme provides shallow rendering with the shallow() method. shallow() takes a component as an argument and returns a wrapper around the rendered component with helper methods used for testing4. The following unit test performs a shallow render, proving that the component successfully renders.

import React from 'react'; import { shallow } from 'enzyme'; import App from '../src/App'; it('renders', () => { const wrapper = shallow(<App/>); expect(wrapper.exists()).toBe(true); });

In this example the wrappers exists() method is called, which returns true if the wrapper's rendered component contains one or more elements (nodes)5. On the Jest side of things, wrapper.exists() was expected to be true.

The wrapper object returned from shallow() has an extensive API that satisfies most testing needs. Shallow rendering often falls short of testing requirements when DOM API logic needs to be tested. shallow() doesn't simulate rendering on the DOM, so none of its APIs are accessible from the wrapper object. Components must be fully rendered with Enzyme's mount() function in order to be tested with the DOM. Full DOM rendering falls under integration tests, which I will cover in the next section.

A common unit test for components is to assert that certain text or classes appear in the rendered elements. These tests can also be fulfilled by snapshots depending on your personal preference. The following test traverses the rendered nodes in a component and asserts that the rendered text is as expected.

it('renders message', () => { const wrapper = shallow(<DerivedFromPropsRefactored show={true} />); expect(wrapper.find('.secret-code')).toHaveLength(1); expect(wrapper.find('.secret-code-classified')).toHaveLength(0); expect(wrapper.find('.secret-code').childAt(0).text()).toEqual('You have a beautiful heart.'); });

There are many other unit tests that can be performed on React components. When I first started using Enzyme, familiarizing myself with the shallow render wrapper functions was crucial to writing quality unit tests.

Integration tests occur when units of code are combined and tested together as a group. Integration tests build upon the Jest and Enzyme fundamentals used for unit testing. In this section, I'll show some interesting tests from my React 16.3 application. I encourage you to check out and adjust my tests on GitHub to meet your specific needs.

Integration tests are performed on React components using Enzyme's mount() method. mount() simulates a full DOM rendering with access to the DOM API. With that in mind, integration tests are useful for code that accesses the DOM API. In React, Refs serve this purpose. The following test checks if the CSS style attached to a DOM node changes after a button is clicked.

import React from 'react'; import { shallow, mount } from 'enzyme'; import RefSamples from '../../src/createref/RefSamples'; it('lights up a light bulb on focus', () => { const wrapper = mount(<RefSamples />); const button = wrapper.find('.aj-outlined-button'); const lightBulb = wrapper.find('.ref-sample'); // The initial filter value will be empty instead of 'brightness(4)' as defined in my CSS file // because Jest/Enzyme don't load CSS stylesheets. expect(lightBulb.getDOMNode().style.filter).toEqual(''); button.simulate('click'); expect(lightBulb.getDOMNode().style.filter).toEqual('brightness(5)'); // Alternative way to get the style value through the DOM API. expect(lightBulb.getDOMNode().style.getPropertyValue('filter')) .toEqual('brightness(5)'); });

Enzyme provides access into a DOM node of a React element with the getDOMNode() method6. style.filter and style.getPropertyValue() are part of the DOM API, not React or Enzyme.

This example also demonstrates another important feature of React integration tests - event simulation. If React code handles or responds to DOM events like onClick, integration tests are needed. I used Enzyme's simulate(event) function to simulate clicking a button.

If you would like to learn more about React Refs and this integration test, you can view the React source code, unit test code, and my React 16.3 demo which explains the use of Refs.

A common practice in unit tests is mocking. Mocking is when a function or external dependency is replaced with something that imitates its behavior. Mocking helps isolate unit tests from the side effects of code dependencies. For example, a function that makes an external REST API call can be mocked to simply return an object.

In my React 16.3 application, I needed to mock the behavior of React Router navigation. React Router navigates users to different web pages by placing a new value in the browser history. For example, from a home page (jarombek.com) React Router can push a new value (jarombek.com/resume) to the browser history, navigating the user to a page displaying my resume in the process. My application uses React Router's useHistory() hook method to provide navigation. The code which tests its behavior mocks useHistory() with jest.mock().

import React from 'react'; import { shallow, mount } from 'enzyme'; import { useHistory } from 'react-router-dom'; import FeaturePage from '../src/FeaturePage'; // Mock react router's useHistory() hook before the tests execute. jest.mock('react-router-dom', () => { const historyObj = { push: jest.fn() }; return { ...jest.requireActual('react-router-dom'), useHistory: () => historyObj } }); describe('integration tests', () => { it('calls React Router push() when clicking the back button', () => { const wrapper = mount(<FeaturePage/>); const pushSpy = jest.spyOn(useHistory(), 'push').mockImplementation(); const navCircle = wrapper.find('.aj-nav-circle'); expect(pushSpy).not.toHaveBeenCalled(); expect(pushSpy).toHaveBeenCalledTimes(0); navCircle.simulate('click'); expect(pushSpy).toHaveBeenCalled(); expect(pushSpy).toHaveBeenCalledTimes(1); expect(pushSpy).toHaveBeenCalledWith('/'); }); });

You can see this code mocked the entire react-router-dom module, and inside it also mocked useHistory with jest.fn(). In the integration test, I spied on the useHistory function, providing information such as the number of times its been called and with which arguments7. This test proves that clicking on a navigation button pushes the home page path ('/') onto the browser history.

I'm still learning how to use mocks and spies in Jest, so the code above can likely be improved upon!

In my experience, snapshot tests are much simpler than unit tests and integration tests. A snapshot test passes if a rendered component matches an existing snapshot, and fails if a rendered component changed since an existing snapshot was taken.

The first step when implementing snapshot tests is to create a test case. The following is a snapshot test for a button component used in my React 16.3 demo.

import React from 'react'; import ButtonWrapper from '../../src/ButtonWrapper'; import renderer from 'react-test-renderer'; it('renders correctly', () => { const tree = renderer.create(<ButtonWrapper>Snapshot Test</ButtonWrapper>).toJSON(); expect(tree).toMatchSnapshot(); });

When running the snapshot test for the first time, a snapshot file is created with the rendered elements as they would appear on the React virtual DOM. For example, ButtonWrapper is a functional component:

const ButtonWrapper = (props) => { const {children, onClick, ref} = props; return ( <button className="button-wrapper" type="button" onClick={onClick} ref={ref}> {children} </button> ); };

When the snapshot test for ButtonWrapper runs, the following snapshot file is generated:

// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders correctly 1`] = ` <button className="button-wrapper" type="button" > Snapshot Test </button> `;

All future tests don't overwrite the snapshot file, instead comparing the result of the snapshot test to it. If the snapshots mismatch, the test fails.

There are two main scenarios that cause a snapshot test to fail. One scenario is when the component under test was incorrectly modified, causing the UI to change. In this scenario, the course of action is to fix the component causing the failure. Another scenario is when the component was intentionally modified and the UI is expected to change. In this scenario, the snapshot files should be updated to match the new UI rendering. This is achieved by executing jest --updateSnapshot from the command line.

I've only found snapshots helpful when the developer or team makes it a point of emphasis to validate snapshots before releasing new code. Otherwise, snapshot files will always be out of date and worthless for testing purposes.

Used together, unit, integration, and snapshot tests help detect regressions and bugs in a React codebase. They ensure that all UI changes released are intentional, not due to an unwanted side effect. I use Jest and Enzyme in all my React applications. You can view my React 16.3 test suite on GitHub.