Write a post

DOM Testing React Applications with Jest

Published Dec 05, 2016Last updated Jan 18, 2017
DOM Testing React Applications with Jest

Jest is a test runner for testing Javascript code.

A test runner is a piece of software that looks for tests on your codebase and runs them. It also takes care of displaying the result on the CLI interface. And when it comes to Jest, it boasts of having very fast test execution as it runs tests in parallel.

Key Features

  • Easy configuration: For trivial cases, jest can be run without any configuration. And if needed, Jest also provides a variety of ways to customize your tests by providing relevant parameters in package.json or as a separate jest.config.js file.
jest --config jest.config.js
  • Snapshot testing:
    Much has been written about this kind of testing. Jest provides a way to take "snapshots" of your DOM components, which is essentially a file with the HTML of the component's render stored as a string. The snapshots are human readable and act as an indicator of any DOM change due to component code changes.

  • Mocking:
    Jest provides easy ways to auto-mock or manually mock entities. You can mock functions and modules which are irrelevant to your test. Jest also provides fake timers for you to control the setTimeout family of functions in tests.

  • Interactive CLI:
    Jest has a watch mode which watches for any test or relevant code changes and re-runs the respective tests. The watch has an interactive feature to it wherein it provides options to run all tests or fix snapshots while in watch mode itself. This is incredibly convenient.

  • Coverage metrics: Jest provides some awesome coverage metrics right out of the box.

    It can be configured to show different levels of these metrics. Also, it is possible to cap the coverage to a threshold and bail or error out if the threshold is breached. Your tests, however, will complete very slowly if this feature is enabled.

jest --coverage

Setting Up Jest

In this post, we will look at installing Jest and getting started with DOM testing a React application. Theoretically, these techniques could be used with other view frameworks like deku, preact and so on. But there may be challenges with finding proper rendering utils which are mature and feature rich.

Install jest for your project

npm install --save-dev jest

Add npm test script

package.json

{
    ...
    scripts: {
        ...
        test: 'jest --watch --verbose'
    }
    ...
}

DOM Testing

In the following examples, we will mostly test trivial methods of general testing areas for a front-end application.

This involves:

  • Basic DOM testing
  • Event simulation and testing
  • Window events simulation

The sample code can be found at jest-blog-samples

Basic DOM Test

The app itself consists of a React component App which prints out a div with text "Hello jest from react".

index.js

import React, {Component} from 'react';
import {render} from 'react-dom';

export default class App extends Component {
    render() {
        return <div>Hello jest from react</div>;
    }
}

render(<App/>, document.body)

The above app is bundled using Webpack

My first test is to check if the App component renders out the div.

index.test.js

import React from 'react';
import App from '../index';
import {shallow} from 'enzyme';


describe('The main app', () => {
    it('the app should have text', () => {
        const app  = shallow(<App/>);
        expect(app.contains(<div>Hello jest from react</div>)).toBe(true);
    })
})

Let's break this down.

  • We need to import react (for the purpose of using JSX) and the App (to instantiate and test) source.
  • Enzyme is JavaScript test utility that helps render React components for testing. Funny enough, a lot of people think that they should pick between React and Enzyme, and this tutorial will clarify this use. But for this tutorial, we will use it to shallow render our App component and inspect the resulting tree.
  • We describe a test suite named "The main app."
  • Our first test is named "the app should have text."
  • We shallow render our component and store it in a const.
  • Add an expect assertion for the app to contain the expected div.

Run the test with Jest

Assuming Jest is installed locally for the application and you have configured the test script in package.json as recommended, let's first start the test runner.

npm run test 

This automatically finds the tests to be run. If required, one can specify testPathDirectories and testIgnorePaths.

 FAIL  __tests__/index.test.js
  ● Test suite failed to run

    SyntaxError: /Users/pavithra.k/Workspace/jest-blog-samples/dom-testing/__tests__/index.test.js: Unexpected token (8:29)
       6 | describe('The main app', () => {
       7 |     it('the app should have text', () => {
    >  8 |         const app  = shallow(<App/>);
         |                              ^
       9 |         expect(app.contains(<div>Hello jest from react</div>)).toBe(true);
      10 |     })
      11 | })
      
      at Parser.pp.raise (node_modules/babylon/lib/index.js:4215:13)

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        1.07s
Ran all test suites.

We have our first failing test!

The error mainly points at a syntax error. This is because our test code is written in ES2015 and it is not transpiled and ready for Node.

This means Jest needs to preprocess our test code and imported source code to ES5. For this, Jest has great integration with Babel

Add Babel Support

Add a .babelrc file to your application. In this, you can specify the Babel plugins required to transpile every ES2015 feature that your application uses.
babel provides excellent presets for react and es2015. Presets are just a collection of relevant babel plugins for a logical set of transformations.

Jest comes with an inbuilt Babel preprocessor, called babel-jest. Essentially, this just transpiles all the tests before running them, if there is a .babelrc file present in the application.

Now on running Jest, we have:

 PASS  __tests__/index.test.js
  The main app
    ✓ the app should have text (8ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.175s
Ran all test suites.

Test an event execution

Using Enzyme, we can simulate events in our rendered components.

We intend to build an App Component that is clickable. It should change the text displayed after the body is clicked on.

To test this component, we need to:

  • Test for the default content on the first load.
  • Test for the first toggle of click and expect the change in text.
  • Test for the second toggle of click and again expect the change in text from the default.

tests/index.test.js

import React from 'react';
import App, {DEFAULT_TEXT, CLICKED_TEXT} from '../index';
import {shallow} from 'enzyme';

describe('The main app', () => {
    let app;
    beforeEach(() => {
        app  = shallow(<App/>);
    })

    it('the app should have text', () => {
        expect(app.text()).toBe(DEFAULT_TEXT);
    })
    it ('should change text on click', () => {
        app.simulate('click');
        expect(app.text()).toBe(CLICKED_TEXT);
        app.simulate('click');
        expect(app.text()).toBe(DEFAULT_TEXT);
    })
})

Jest provides us with the following global testing methods

describe(name,fn), it(name, fn), test(name, fn)

These are the basic tools in a JavaScript testing environment's arsenal.

  • describe is used to group a set of related tests together into a test suite.
  • it or test is used to write your actual test where you can build an independent environment and test particular units or behavior.
afterEach(fn)/beforeEach(fn)/afterAll(fn)/beforeAll(fn)

These functions run before/after each test or before/after the test suite. You can group all your initialization and tear down the code in these hooks which are called during a test cycle.

Keeping the above info in context, we write a beforeEach function which creates a shallow rendering of the Button App.
After this, we write our tests for each of the behavior we expect out of the component. Enzyme has a bunch of helper APIs to inspect a rendered object. We can use these to check rendered DOM for desired changes.

The corresponding React component would look like this:

index.js

import React, {Component} from 'react';
import {render} from 'react-dom';

export const DEFAULT_TEXT = "Hello jest from react";
export const CLICKED_TEXT = "This has been clicked";

export default class App extends Component {
    constructor() {
        super();
        this.state = {
            clicked: false
        }
        this.handleClick = this.handleClick.bind(this);
    }
    render() {
        return <div onClick={this.handleClick}>{this.state.clicked ? CLICKED_TEXT : DEFAULT_TEXT}</div>;
    }
    handleClick() {
        this.setState({
            clicked: !this.state.clicked
        })
    }
}

render(<App/>, document.body)

window event testing

Many times, our apps have the functionality to respond to outer stimuli like window events. We would like to test if our app behaves correctly under these conditions.
For this case, we will have to mock the document and document.window object.

We will use JSDOM API which provides a way of faking document and window objects.

npm install --save-dev jsdom

We are now going to use JSDOM to wire up our fake objects. Since this activity needs to happen before every test as any of them could want the window object, we will write this code at a setupFile which can be configured in Jest.

Go to your package.json or jest.config.js file and add the following option

package.json

jest: {
    "setupFiles" : ["<rootDir>/setupFile.js"]
}

setupFile.js

import {jsdom} from 'jsdom';

const documentHTML = '<!doctype html><html><body><div id="root"></div></body></html>';
global.document = jsdom(documentHTML);
global.window = document.parentWindow;

global.window.resizeTo = (width, height) => {
  global.window.innerWidth = width || global.window.innerWidth;
  global.window.innerHeight = width || global.window.innerHeight;
  global.window.dispatchEvent(new Event('resize'));
};

Note that the above code also gives us a div element with the ID root. We can now mount our react component onto this element for inclusion in the DOM.

Also, resizeTo will not be defined by default in this fake window object. It will return undefined. Hence, we need to mock up the whole function as well. You will need to do this to trigger any event on the window from your test.
Currently, I'm only bothered with the innerWidth and innerHeight parameters. Hence, I will only update them as part of the mock.

Our App code for this test is a React component that responds to window's resize event. If the resize results in the innerWidth > 1024 then it changes its width to 350. If not width = 300.

src/App.js


import React, {Component} from 'react';

export const WIDTH_ABOVE_1024 = 350;
export const WIDTH_BELOW_1024 = 300;
export const THRESHOLD_WIDTH = 1024;

export default class App extends Component {
    constructor() {
        super();
        this.state = {
            width: window.innerWidth > THRESHOLD_WIDTH ? WIDTH_ABOVE_1024: WIDTH_BELOW_1024
        }
        this.handleResize = this.handleResize.bind(this);
    }
    componentDidMount() {
        window.addEventListener('resize', this.handleResize)
    }
    componentWillUnmount() {
        window.removeEventListener('resize', this.handleResize)
    }
    render() {
        return <div style={{width:this.state.width}}>My width is {this.state.width}</div>;
    }
    handleResize() {
        this.setState({
            width: window.innerWidth > THRESHOLD_WIDTH ? WIDTH_ABOVE_1024 : WIDTH_BELOW_1024
        })
    }
}

For the above component, we again need to test if:

  • Default behaviour.
  • Resize to width > 1024
  • Resize to width <=1024

src/App.test.js

import React from 'react';
import App, {WIDTH_ABOVE_1024, WIDTH_BELOW_1024, THRESHOLD_WIDTH} from './App';
import {mount} from 'enzyme';

describe('The main app', () => {
    let app;
    beforeEach(() => {
        app = mount(<App/>, {attachTo: document.getElementById('root')});
    })

    it('the app should have text', () => {
        var width = window.innerWidth > THRESHOLD_WIDTH ? WIDTH_ABOVE_1024 : WIDTH_BELOW_1024;
        expect(app.text()).toBe("My width is " + width);
        app.unmount();
    })
    it ('should change text on click', () => {
        window.resizeTo(1000, 1000);
        expect(app.text()).toBe("My width is " + WIDTH_BELOW_1024);
        window.resizeTo(1025, 1000)
        expect(app.text()).toBe("My width is " + WIDTH_ABOVE_1024);
        app.unmount();
    })
})

Things to note are:

  • We can totally use the component's constants for testing.
  • Once you mount your app using Enzyme, you will also need to unmount() it at the end of each test.
  • window.resizeTo will trigger a change in window.innerWidth. The app code rerenders as per app logic and we can test for printed width.

Key Takeaways

  • Testing is now way easier than before with Jest.
  • DOM testing with React needs Enzyme as a renderer util and JSDOM as a DOM helper.
  • Mocking can be tricky. It's fine to have hacky mocks.

I hope this article has inspired you to take up testing your application with Jest and you will be motivated to add it to your dev workflow!

Discover and read more posts from pkodmad
get started
Enjoy this post?

Leave a like and comment for pkodmad

4
Angular Form Validation - ngModelController
How to Build a Lightweight Command Runner for the CLI using PHP
Dive into Genetic Algorithms: A 101