Testing with Jest and Enzyme in React — Part 4 (shallow vs. mount in Enzyme)
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.
Conclusion
We now know the pros and cons of using shallow and mount methods and I hope you can clearly choose which one is best for the test that you are writing.
You can find the work we did in this tutorial in the branch named tutorial-04 in the below git repository.
In the next tutorial, I will dive more into testing with Jest and Enzyme by writing tests for rendering, interactions, and lifecycle method calls. Until then, Goodbye!
Previous Tutorials
📝 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 >