Testing with Jest and Enzyme in React — Part 4 (shallow vs. mount in Enzyme)

Wasura Wattearachchi
7 min readJan 14, 2019

--

Most of us have a problem of clarifying When to use shallow and when to use mount when testing with Enzyme. In this tutorial, I am going to discuss the differences between shallow and mount, and the pros and cons of them.

Before starting the tutorial, I recommend you to have the project named “testing-demo-app” which I used for the previous tutorials, and below is the link to it.

shallow

  • shallow method is used to render the single component that we are testing. It does not render child components.
  • In Enzyme version less than 3, the shallow method does not have the ability to access lifecycle methods. But in Enzyme version 3, we have this ability.
  • Simple shallow calls the constructor, render, componentDidMount (in Enzyme version 3) methods.
  • shallow + setProps call componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate, render, componentDidUpdate (in Enzyme version 3) methods.
  • shallow + unmount call componentWillUnmount method.

mount

  • mount method renders the full DOM including the child components of the parent component that we are running the tests.
  • This is more suitable when there are components which directly interfere with DOM API or lifecycle methods of React.
  • But this is more costly in execution time.
  • Simple mount calls the constructor, render, componentDidMount methods.
  • mount + setProps call componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate, render, componentDidUpdate methods.
  • mount + unmount call componentWillUnmount method.

The difference between shallow and mount using an example

Step 1 — Create a new component named Form.js

  • Open the project named “testing-demo-app”. Create a new component named Form.js in testing-demo-app/src/components folder, with the following content.
import React, { Component } from 'react';

class Form extends Component {

constructor(props) {
super(props);
this.state = {
firstNumber: '',
secondNumber: '',
componentState:'default'
};

this.handleFirstNumber = this.handleFirstNumber.bind(this);
this.handleSecondNumber = this.handleSecondNumber.bind(this);
}

componentDidMount() {
this.setState({ componentState: 'mounted' });
}


handleFirstNumber(event) {
this.setState({ firstNumber: event.target.value });
}

handleSecondNumber(event) {
this.setState({ secondNumber: event.target.value });
}

handleAdd(){
const { firstNumber, secondNumber } = this.state;
this.displayResult(parseInt(firstNumber) + parseInt(secondNumber))
}

handleSubtract(){
const { firstNumber, secondNumber } = this.state;
this.displayResult(parseInt(firstNumber) - parseInt(secondNumber))

}

displayResult(result){
alert(result);
}

render() {
const { firstNumber, secondNumber } = this.state;
const { operator } = this.props;
return (
<form className='form-group'>
<fieldset className='form-group'>
<label className='form-label'>
First Number:
</label>
<input type="text" id="number1" className='form-input' value={firstNumber} onChange={this.handleFirstNumber}/>
</fieldset>
<fieldset className='form-group'>
<label className='form-label'>
Second Number:
</label>
<input type="text" id="number2" className='form-input' value={secondNumber} onChange={this.handleSecondNumber}/>
</fieldset>
<div className='form-group'>
{operator === '+' &&
<button id='formButtonAdd' className='btn' type="button" onClick={() => this.handleAdd()}>Add</button>
}
{operator === '-' &&
<button id='formButtonSubtract' className='btn' type="button" onClick={() => this.handleSubtract()}>Subtract</button>
}
</div>
</form>
);
}

}

export default Form;
  • Go to testing-demo-app/src/App.css and clear the content in that file and add the following css rules.
html {
box-sizing: border-box;
font-size: 16px;
}

*,
*:after,
*:before {
box-sizing: border-box;
}

body {
font: 100% 'Roboto', arial, sans-serif;
background: #f5f5f5;
}

form {
padding: 2rem;
margin-top: 2rem;
margin-right: auto;
margin-left: auto;
max-width: 23.75rem;
background-color: #fff;
border-radius: 3px;
box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1), 0 5px 15px rgba(0, 0, 0, 0.07);
}

h1 {
margin-top: 2%;
margin-bottom: 3.236rem;
text-align: center;
font-size: 1.618rem;
}

.form-group {
padding: 0;
border: 0;
}

.form-group + .form-group {
margin-top: 1rem;
}

label {
display: inline-block;
margin-bottom: .5rem;
font-size: .75rem;
text-transform: uppercase;
-ms-touch-action: manipulation;
touch-action: manipulation;
}

input,
textarea {
display: block;
padding: .5rem .75rem;
width: 100%;
font-size: 1rem;
line-height: 1.25;
color: #55595c;
background-color: #fff;
background-image: none;
background-clip: padding-box;
border-top: 0;
border-right: 0;
border-bottom: 1px solid #eee;
border-left: 0;
border-radius: 3px;
-webkit-transition: all 0.25s cubic-bezier(0.4, 0, 1, 1);
transition: all 0.25s cubic-bezier(0.4, 0, 1, 1);
}

input:focus,
textarea:focus {
outline: 0;
border-bottom-color: #ffab00;
}

textarea {
resize: vertical;
}

.btn {
display: inline-block;
padding: .75rem 1rem;
width: 100%;
margin-top: 1.618rem;
font-weight: 400;
text-align: center;
text-transform: uppercase;
color: #fff;
vertical-align: middle;
white-space: nowrap;
background-color: #950aff;
border: 1px solid transparent;
box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1), 0 5px 15px rgba(0, 0, 0, 0.07);
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-transition: all 0.25s cubic-bezier(0.4, 0, 1, 1);
transition: all 0.25s cubic-bezier(0.4, 0, 1, 1);
}

.btn:focus, .btn:hover {
background-color: #c78eff;
box-shadow: 0 18px 35px rgba(50, 50, 93, 0.1), 0 8px 15px rgba(0, 0, 0, 0.07);
}
.btn:focus {
outline: 0;
}
  • Import App.css file in testing-demo-app/src/App.js by adding the following line in App.js file.
import './App.css';

Step 2— Modify Add.js component

  • Go to testing-demo-app/src/components and add the following content to Add.js file.
import React, { Component } from 'react';
import Form from './Form';

class Add extends Component {
render() {
return (
<div>
<h1>Add Function</h1>
<Form operator='+'/>
</div>
);
}
}

export default Add;

Note — <h1> and <Form> are child components of <Add>

Now when you start the server by typing npm start, you should get an interface like below.

Note — Enter 2 numbers in the FIRST NUMBER and the SECOND NUMBER fields and click on ADD button. A JavaScript alert should be displayed with the result as the addition of entered numbers.

Step 3 — Write and run a test for Add component

  • Go to testing-demo-app/test and modify Add.test.js with the following content.
import Add from '../src/components/Add';
import Form from '../src/components/Form';
let wrapper;beforeEach(() => {
wrapper = shallow(<Add />);
});
describe('<Add /> rendering', () => {
it('should render one <h1>', () => {
expect(wrapper.find('h1')).toHaveLength(1);
});
it('should render one <Form>', () => {
expect(wrapper.find(Form)).toHaveLength(1);
});
it('should render 2 <label>s', () => {
expect(wrapper.find('label')).toHaveLength(2);
});
});
  • Open up a terminal inside the project directory and run the below command in order to run the tests in Add.test.js file.
npm test Add.test.js
  • Now your test should fail like below.

Why this is failed?

  • You can see 2 tests have passed, which are to check the rendering of <h1> and <Form> components.
  • But the test to check the rendering of <label> elements was failed.
  • <h1> and <Form> are child components of <Add> component. (which is what we are testing now)
  • But <label> elements are child components of <Form>.
  • shallow method does not render <label> elements inside <Form> copmonent, which is why this test fails.

Solution

  • Change the shallow method in Add.test.js to mount and run the test again.
beforeEach(() => {
wrapper = mount(<Add />);
});
  • Now the test should pass as follows.
  • The reason for this is, mount renders the full DOM including the child components in the parent component. So, this has fully rendered <Form> with <label>s which we are searching for.

What is the best?

  • It is recommended to use shallow as much as possible because unit tests should be isolated as much as possible.
  • We do not need to check the units (components) inside a unit (component)when running a single test.
  • When you use mount you are automatically exposed to the logic of all the units (components) in your render tree making it impossible to only test the component in question.
  • It is more costly in execution time when using mount, because it requires JSDOM.

render

  • There is another function like shallow and mount, which is render.
  • This has the ability to render to static HTML.
  • It renders the children.
  • But this does not have access to React lifecycle methods.

📝 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 >

--

--