Testing with Jest and Enzyme in React — Part 3 (Best Practices when testing with Jest and Enzyme)

Wasura Wattearachchi
6 min readJan 8, 2019

--

In the previous tutorial, we learned “How to integrate Enzyme with Jest in Testing?”. Before diving into the coding part more, it is better to learn about the “Best Practices when testing with Jest and Enzyme”.

Tip 1 — Create a separate file with the global variables

  • As we have discussed in the previous tutorial, it is appropriate to create a file named setupTests.js in testing-demo-app/test folder with the global variables to be used throughout the tests. Below is an example for a setupTests.js file, which I created for this tutorial series.
import React from 'react';
import Enzyme, { shallow, render, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';;
// React 16 Enzyme adapter
Enzyme.configure({ adapter: new Adapter() });
// Make Enzyme functions available in all test files without importing
global
.React = React;
global.shallow = shallow;
global.render = render;
global.mount = mount;

Tip 2 — Use ‘describe’ and ‘it ’keywords appropriately in tests

  • describe’ helps to decompose/break your test suite into components. In other words, it helps to break the tasks of a component and to visualize the big picture of the tests. Also, you can use nested ‘describe’ statements to further subdivide the test suite.
  • ‘it’ explains each test that you are going to perform. For example, in a component named ‘Add’, can have a test specified as “it(‘should render 2 <input>s’……….”. You should not be able to subdivide tests further when you use ‘it’.

Tip 3 — Main categories to test

  • It is better if you can categorize the tests under ‘describe’ keyword. Following are some examples.
  1. Rendering — You can categorize the tests like rendering components in a parent component. Examples: How many text boxes rendered?, Whether some element is rendered under some condition? etc.
  2. Interactions — You can categorize the tests which help to check the interactions in UI. Examples: onClick method of a button, onChange method, state changes etc.
  3. Lifecycle Method Calls — You can categorize tests which help to know whether a particular lifecycle method is called. Examples: To test componentDidMount method and associated state changes etc.

Please find the below example with categorizing the tests.

describe('<CommentAdd /> rendering', () => {
it('should render a <TextField /> to type the comment', () => {
expect(wrapper.find(TextField)).toHaveLength(1);
});

it('should render a <Typography /> to display the maximum length of the comment', () => {
expect(wrapper.find(Typography)).toHaveLength(1);
});
});

describe('<CommentAdd /> interactions', () => {
it('should call the onClick function when \'Add Comment\' button is clicked', () => {
const mockedHandleClickAddComment = jest.fn();
wrapper.instance().handleClickAddComment = mockedHandleClickAddComment;
wrapper.find(Button).first().props().onClick();
expect(mockedHandleClickAddComment).toHaveBeenCalledTimes(1);
});

it('should change the state commentText and currentlength when the onChange function of the TextField is invoked', () => {
wrapper.find(TextField).simulate('change',
{ target: { value: 'New Comment' } }
);
expect(wrapper.state('commentText')).toEqual('New Comment');
expect(wrapper.state('currentLength')).toEqual('New Comment'.length);
});
});
describe('<CommentAdd /> lifecycle method invocations', () => {
it('should change the state componentState componentDidMount method is invoked', () => {
expect(wrapper.state('componentState')).toEqual('mounted');
});
});

Tip 4 — Things to be done Before and After tests

  • It is better if you can structure what should be done before or after some set of tests. You can use the following keywords for that.
beforeEach(() => { someInitializationFunction(); });  
afterEach(() => { someClearFunction(); });
beforeAll(() => { someOneTimeInitializationFunction(); });
afterAll(() => { someOneTimeClearFunction(); });
  • Important — It is better if we can run each ‘it’ block without depending on others in an isolated manner. In here sometimes beforeAll() or afterAll() methods can cause harm. So make sure to use them wisely.

Tip 5 — Passing props

  • When there are more than one props to be passed it is better to use a separate function to create the props (createTestProps()) and assigned them to a variable (props) by calling the function inside beforeAll() or beforeEach() or in any place you need. Below is an example of such function usage.
function createTestProps(props) {
return {
apiId: '6e770272-212b-404e-ab9c-333fdba02f2f',
cancelButton: true,
allComments: [],
theme: { custom: { maxCommentLength: 1300 } },
...props,
};
}
let wrapper, props;beforeEach(() => {
props = createTestProps();
wrapper = shallow(<Comment {...props} /> );
});
  • Important — The purpose of having props to be passed as an argument to createTestProps() method is, to provide the facility of simply overriding/adding a prop value in the middle of a test case. Check below code segment.
it('should render only one <Button /> to display only the save option, if cancelButton property is false ', () => {
wrapper = shallow(<CommentAdd {...props} cancelButton={false} /> );
expect(wrapper.find(Button)).toHaveLength(1);
});
});
  • In the above example, you can see the cancelButton value is overrided.

Full Example

Below is a sample test file which I wrote by considering all the facts that we discussed above.

import { TextField, Button, Typography } from '@material-ui/core';
import CommentAdd from '../../src/Comments/CommentAdd';

/**
* Initialize common properties to be passed
* @param {*} props properies to be override
*/
function createTestProps(props) {
return {
apiId: '6e770272-212b-404e-ab9c-333fdba02f2f',
cancelButton: true,
allComments: [],
theme: { custom: { maxCommentLength: 1300 } },
...props,
};
}

let wrapper, props;

beforeEach(() => {
props = createTestProps();
wrapper = shallow(<Comment {...props} /> );
});

describe('<CommentAdd /> rendering', () => {
it('should render a <TextField /> to type the comment', () => {
expect(wrapper.find(TextField)).toHaveLength(1);
});

it('should render a <Typography /> to display the maximum length of the comment', () => {
expect(wrapper.find(Typography)).toHaveLength(1);
});

it('should render 2 <Button /> s to display the save and cancel options, if cancelButton property is true ', () => {
expect(wrapper.find(Button)).toHaveLength(2);
});

it('should render only one <Button /> to display only the save option, if cancelButton property is false ', () => {
wrapper = shallow(<CommentAdd {...props} cancelButton={false} /> );
expect(wrapper.find(Button)).toHaveLength(1);
});
});

describe('<CommentAdd /> interactions', () => {
it('should call the onClick function when \'Add Comment\' button is clicked', () => {
const mockedHandleClickAddComment = jest.fn();
wrapper.instance().handleClickAddComment = mockedHandleClickAddComment;
wrapper.find(Button).first().props().onClick();
expect(mockedHandleClickAddComment).toHaveBeenCalledTimes(1);
});

it('should change the state commentText and currentlength when the onChange function of the TextField is invoked', () => {
wrapper.find(TextField).simulate('change',
{ target: { value: 'New Comment' } }
);
expect(wrapper.state('commentText')).toEqual('New Comment');
expect(wrapper.state('currentLength')).toEqual('New Comment'.length);
});
});
describe('<CommentAdd /> lifecycle method invocations', () => {
it('should change the state componentState componentDidMount method is invoked', () => {
expect(wrapper.state('componentState')).toEqual('mounted');
});
});

📝 Read this story later in Journal.

🗞 Wake up every Sunday morning to the week’s most noteworthy Tech stories, opinions, and news waiting in your inbox: Get the noteworthy newsletter >

--

--