React Testing with Airbnb's Enzyme, Jest and Babel
Mark Abeto
Posted on August 28, 2019
Hi Guys Good Day!
ok, guys first We gonna configure Jest, Enzyme from the ground up. So you can know what modules or packages we're gonna use.
Do this in the command line on your desktop.
md testing-with-enzyme && cd testing-with-enzyme
md testing-with-enzyme - Makes a directory with a name of testing-with-enzyme
&& - runs the second command if the first command does not throw an error
cd testing-with-enzyme - Changes the current directory to testing-with-enzyme
npm init --y && npm i -D @babel/preset-env @babel/preset-react
@babel/plugin-proposal-class-properties @types/jest jest
enzyme enzyme-adapter-react-16 && npm i -P react react-dom
ok, I'm not gonna explain all these packages but we're gonna all of these packages to work with enzyme and jest.
type nul > babel.config.js && type nul > jest.config.js && md Tests && md components
type nul for Windows OS. touch for UNIX Systems.
Our babel.config.js file.
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: ['@babel/plugin-proposal-class-properties']
}
Our jest.config.js file.
module.exports = {
rootDir: '.',
displayName: {
name: 'enzyme-setup',
color: 'blue'
},
runner: 'jest-runner',
verbose: true,
errorOnDeprecated: true,
roots: ['./Tests'],
moduleFileExtensions: ['js', 'jsx'],
setupFilesAfterEnv: ['<rootDir>Tests/setupTest.js']
}
Inside our Tests folder make a setupTest.js file.
import { configure } from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
configure({
adapter: new Adapter()
})
Inside the components folder make 4 files.
type nul > App.js && type nul > Form.js && type nul > Header.js && type nul > List.js
Our Header.js file.
import React from 'react'
export default function Header({ message, handleToggleTheme, theme }) {
return (
<div className="header">
<h1>{message}</h1>
<button className="right" onClick={handleToggleTheme}>
<i className={theme}></i>
</button>
</div>
)
}
Our Form.js file.
import React from 'react'
export default function Form({ handleChange, value, handleClick }) {
return (
<div className="form">
<input
className="form-control"
type="text"
onChange={handleChange}
value={value}
/>
<button className="btn" onClick={handleClick}>
Submit
</button>
</div>
)
}
Our List.js file.
import React from 'react'
export default function List({ items }) {
return (
<ul className="list">
{items.map(item => (
<li className="list-item" key={item}>{item}</li>
))}
</ul>
)
}
Our App.js file.
import React, { Component } from 'react'
import Header from './Header'
import List from './List'
import Form from './Form'
export default class App extends Component {
state = {
listItem: '',
items: [],
isDarkTheme: false
}
handleChange = ({ target: { value } }) => {
this.setState({
listItem: value
})
}
handleClick = () => {
this.setState({
items: [...this.state.items, this.state.listItem],
listItem: ''
})
}
handleToggleTheme = () => {
this.setState({
isDarkTheme: !this.state.isDarkTheme
})
}
render() {
const theme = this.state.isDarkTheme ? 'dark' : 'light'
return (
<div className={theme}>
<Header
theme={theme}
message={this.props.message}
handleToggleTheme={this.state.handleToggleTheme}
/>
<Form
handleChange={this.state.handleChange}
value={this.state.listItem}
handleClick={this.state.handleClick}
/>
<List items={this.state.items} />
</div>
)
}
}
App.defaultProps = {
message: 'Hello World'
}
Inside the Tests folder make an index.test.js file.
import React from 'react'
import App from '../components/App'
import Header from '../components/Header'
import Form from '../components/Form'
import List from '../components/List'
import { shallow, mount } from 'enzyme'
describe('Test App component', () => {
let wrapper;
beforeAll(() => {
wrapper = shallow(<App />)
})
it('should not return an error', () => {
expect(wrapper).toMatchSnapshot()
console.log(wrapper.debug())
})
})
Then in your terminal run this command
npm t
If it does not throw an error and it passed then your good to go.
What's the difference between shallow rendering and full mount rendering?
There's this very useful method debug that both shallow and mount provides us.
Try updating our index.test.js file to look like this.
import React from 'react'
import App from '../components/App'
import Header from '../components/Header'
import Form from '../components/Form'
import List from '../components/List'
import { shallow, mount } from 'enzyme'
describe('Test App component', () => {
let shallowWrapper, mountWrapper;
beforeAll(() => {
shallowWrapper = shallow(<App />)
mountWrapper = mount(<App />)
console.log(shallowWrapper)
console.log(mountWrapper)
})
})
Structure using the debug method.
The first console.log looks like this.
console.log Tests/index.test.js:12
<div className="light">
<Header theme="light" message="Hello World" handleToggleTheme={[Function]}
/>
<Form handleChange={[Function]} value="" handleClick={[Function]} />
<List items={{...}} />
</div>
The second console.log looks like this.
console.log Tests/index.test.js:13
<App message="Hello World">
<div className="light">
<Header theme="light" message="Hello World" handleToggleTheme={[Function]}>
<div className="header">
<h1>
Hello World
</h1>
<button className="right" onClick={[Function]}>
<i className="light" />
</button>
</div>
</Header>
<Form handleChange={[Function]} value="" handleClick={[Function]}>
<div className="form">
<input className="form-control" type="text" onChange={[Function]} value="" />
<button className="btn" onClick={[Function]} />
</div>
</Form>
<List items={{...}}>
<ul className="list" />
</List>
</div>
</App>
The debug method basically gives us the structure of our component. When we use it on shallow it does not give us the full structure of our component we don't see the JSX structure of our Header,Form, and List component but when we use it on mount it gives us the full structure of our component down to every JSX element that our child components used.
Useful methods that Enzyme provides us.
at(index : number)
Returns a wrapper element based on the index is given.
The difference between using shallow and mount on our App component.
it('should have an "App" component "at" index of 0', () => {
let wrapper = shallow(<App />);
expect(wrapper.at(0).type()).toBe(App);
});
it('should return an App', () => {
let wrapper = mount(<App />);
expect(wrapper.at(0).type()).toBe(App)
});
The first test failed but the second test passed the reason to this is that the type of element at index 0 in our shallowed component is div, not App but in our mountend component is App refer to the Structure Section changing the App to div in the shallowed test will make the test passed.
childAt(index : number)
Returns a new wrapper of the child at the specified index.
it('should have a child component of type "Header" at "index" of 0', () => {
let wrapper = shallow(<App />);
expect(wrapper.childAt(0).type()).toBe(Header);
});
it('should have a child element of type "div" at "index" of 0', () => {
let wrapper = mount(<App />);
expect(wrapper.childAt(0).type()).toBe('div')
});
Base on the Structure of our shallowed App the first child should be Header and in our mounted App the first child should be div. These two tests should passed.
find(selector : EnzymeSelector)
Basically finds every node that matches the given selector.
Selectors.
- find('div') = finds every 'div' element on the current wrapper.
- find('div.something') = finds every 'div' element with a class of 'something' on the current wrapper.
find('div[title="okinawa"]) = finds every 'div' element with an attribute of "title" with a value of "okinawa".
find('#okinawa') = find every element with an id of "okinawa".
find('.okinawa') = finds every element with a class of "okinawa".
find('div#okinawa > span') = finds every 'span' element that is the
direct child of a "div" with an id of "okinawa"find('div.okinawa + span') = finds every 'span' element that is placed after a "div" element with a class of "okinawa"
find('div.okinawa span') = finds every 'span' element that is inside a "div" element with a class of "okinawa"
find(SomeComponent) = finds every element with a contrustor of "SomeComponent"
function App({ children }){
return (
<div>
{children}
</div>
)
}
function SomeComponent(){
return (
<div>
<h1>
Hi!
</h1>
</div>
)
}
it('should have length of "1" when finding "SomeComponent" comp', () => {
const wrapper = shallow(<App>
<SomeComponent />
</App>
)
expect(wrapper.find(SomeComponent)).toHaveLength(1);
});
You can find all the valid selectors here.
closest(selector : EnzymeSelector)
finds the closest parent that matches the selector. It traverses every node up starting with itself.
it('should have an h1 with a text of "Hello World"', () => {
let wrapper = shallow(<App />);
expect(wrapper.find(Header).closest('div.light')).toHaveLength(1);
});
it('should have a parent element of "div" with a class of "light"', () => {
let wrapper = mount(<App />);
expect(wrapper.find(Header).closest('div.light')).toHaveLength(1);
})
These two tests should pass.
contains(node : node | nodes[])
Tests if the containing wrapper has a matching child or children.
it('should have a node of <Header /> and the right props', () => {
let wrapper = shallow(<App />);
expect(wrapper.contains(
<Header theme="light" message="Hello World" handleToggleTheme=
{wrapper.instance().handleToggleTheme} />
)).toBeTruthy();
});
it('should contain these two nodes', () => {
const wrapper = mount(<App />);
expect(wrapper.contains([
<h1>Hi</h1>,
<button className="right" onClick={wrapper.instance().handleToggleTheme}>
<i className="light" />
</button>
])).toBeTruthy();
})
We're using the instance() method to get the reference of the handleToggleTheme function out of that component instance. More on the instance method later. These tests should pass.
containsAllMatchingElements(nodes: nodes[])
Must match all the nodes on the current wrapper.
it('should have these two nodes when shallow mounting', () => {
let wrapper = shallow(<App />);
wrapper.setState({ listItem: '1' })
expect(wrapper.containsAllMatchingElements(
[
<Form handleChange={wrapper.instance().handleChange} value="1" handleClick={wrapper.instance().handleClick} />,
<Header theme="light" message="Hello World" handleToggleTheme={wrapper.instance().handleToggleTheme} />
]
)).toBeTruthy();
});
it('should have these two nodes when mounting', () => {
let wrapper = mount(<App />);
expect(wrapper.containsAllMatchingElements([
<h1>Hi</h1>,
<button className="right" onClick={wrapper.instance().handleToggleTheme}>
<i className="light" />
</button>
])).toBeTruthy();
})
We're using setState to update the value of a property in our state. It works the same as React's setState. this.setState({property: newValue})
. These tests should pass.
containsAnyMatchingElements(nodes: nodes[])
Must match at least one of the nodes on the current wrapper.
it('should this Form with the right props', () => {
expect(wrapper.containsAnyMatchingElements(
[
<Form handleChange={wrapper.instance().handleChange} value="1" handleClick={wrapper.instance().handleClick} />,
]
)).toBeTruthy();
});
it('should return true because the "i" element is right while "div" element is not the right structure', () =>{
expect(wrapper.containsAnyMatchingElements([
<div className="form">
</div>,
<i className="light" />
])).toBeTruthy();
});
You're wondering why we have a value of "1" in the shallowed part that's because we used setState on the previous section and updated our listItem to have a value of 1. These tests should pass.
first()
Behaves like at(0) refer.
hasClass(class:string)
Tests if the current node has prop of className and checks the value.
it('should have a class of "light"', () => {
let wrapper = shallow(<App />);
expect(wrapper.hasClass('light')).toBeTruthy();
});
it('should have a class of "form-control"', () =>
wrapper = mount(<App />);
{
expect(wrapper.find(Form).find('#form').childAt(0).hasClass('form-control')).toBeTruthy();
})
These tests should pass.
html()
returns the raw html string of the current wrapper.
it('should return the correct string', () => {
let wrapper = shallow(<App >);
expect(wrapper.childAt(2).html()).toBe('<ul class="list"></ul>')
});
it('should have an element with an id of "form"', () => {
let wrapper = mount(<App >);
wrapper.setProps({ message: 'Hi' });
expect(wrapper.find('h1').html()).toBe('<h1>Hi</h1>')
})
These tests should pass too.
instance()
returns the current class instance of the current wrapper it returns null when used on a functional component. the instance method can only be used on the root node.
it('should be an instance of App', () => {
let wrapper = shallow(<App />);
expect(wrapper.instance()).toBeInstanceOf(App);
});
it('should be an instance of App', () => {
let wrapper = mount(<App />);
expect(wrapper.instance()).toBeInstanceOf(App);
});
These tests should pass.
invoke(functionPropName)(..arguments)
it('should have a prop of "value" with a value of "12344"', () => {
let wrapper = shallow(<App />);
wrapper.find(Form).invoke('handleChange')({ target: { value: '12344' } });
expect(wrapper.find(Form).prop('value')).toBe('12344');
});
it('should truthy value of prop "isDarkTheme"', () => {
let wrapper = mount(<App />);
wrapper.find(Header).invoke('handleToggleTheme')()
expect(wrapper.state('isDarkTheme')).toBeTruthy();
})
These tests should pass too. I think you're wondering I'm passing an object with a target property which has a value of an object with another property of value because my handleChange function looks like this
handleChange = ({ target: { value } }) => {
this.setState({
listItem: value
})
}
is(selector: EnzymeSelector)
Checks if the selector matches the current wrapper.
it('should return false when checking with ".is"', () => {
let wrapper = shallow(<App />);
expect(wrapper.find(List).find('ul').is('.list')).toBeFalsy();
});
it('should return true when checking with ".is"', () => {
let wrapper = mount(<App />);
expect(wrapper.find(List).find('ul').is('.list')).toBeTruthy();
});
The reason that the first test failed and thrown an error because of the reason
that our element structure when using shallow looks like this
<div className="light">
<Header theme="light" message="Hello World" handleToggleTheme={[Function]}
/>
<Form handleChange={[Function]} value="" handleClick={[Function]} />
<List items={{...}} />
</div>
It does not render the ul element but when we use it on mount it works.
isEmptyRender()
returns true if the current wrapper returns null
or false
.
it('should not be falsy because "App" does not return neither null or false', () => {
let wrapper = shallow(<App />);
expect(wrapper.isEmptyRender()).toBeFalsy();
});
it('should return "Nothing" literally', () => {
class Nothing extends React.Component {
render() {
return (
null
)
}
}
let wrapper = mount(<Nothing />);
expect(wrapper.isEmptyRender()).toBeTruthy();
});
These tests should pass. The second test pass due to the reason we returned null on the render method.
key()
returns the key value of the current wrapper.
it('should have a prop of items with a length of 2 and a key value of "japan"', () => {
let wrapper = mount(<Form />);
let form = wrapper.find(Form);
form.invoke('handleChange')({ target: { value: 'okinawa' } });
form.invoke('handleClick')();
form.invoke('handleChange')({ target: { value: 'japan' } });
form.invoke('handleClick')();
expect(wrapper.find(List).prop('items')).toHaveLength(2);
expect(wrapper.find(List).find('ul').childAt(1).key()).toBe('japan');
});
last()
returns the last node base on the current selected wrapper.
it('should return the last child type which is "List"', () => {
let wrapper = shallow(<App />);
expect(wrapper.children().last().type()).toBe(List);
});
it('should return the last child type which is "div"', () => {
let wrapper = mount(<App />)
expect(wrapper.children().last().type()).toBe('div');
});
name()
returns the "name" of current wrapper.
it('should return a name with a value of "div"', () => {
let wrapper = shallow(<App />);
expect(wrapper.name()).toBe('div');
});
it('should return a name with a value of "App"', () => {
let wrapper = mount(<App />);
expect(wrapper.name()).toBe('App');
});
Again, refer to the Structure Section if your having a little problem understanding.
filter(selector: EnzymeSelector)
it returns a new wrapper based on the selector given.
it('should have a prop of "item" with length of 3', () => {
let wrapper = mount(<App />);
let form = wrapper.find(Form);
let values = ["ohio", "usa", "amawa"];
values.forEach((value) => {
form.invoke('handleChange')({ target: { value } });
form.invoke('handleClick')();
})
expect(wrapper.find(List).find('ul li').filter('.list-item')).toHaveLength(3);
});
});
props()
returns the prop object of the current wrapper
it('should have a prop "items" with a value of []', () => {
let wrapper = shallow(<App />);
expect(wrapper.find(List).props().items).toEqual([]);
});
it('should have a prop "message" with a value of "Hello World"', () => {
let wrapper = mount(<App />);
expect(wrapper.find(Header).props().message).toBe("Hello World");
});
prop(key:string)
return the value of the property of the current wrapper.
it('should have a prop "items" with a value of []', () => {
let wrapper = shallow(<App />);
expect(wrapper.find(List).prop('items')).toEqual([]);
});
it('should have a prop "message" with a value of "Hello World"', () => {
let wrapper = mount(<App />);
expect(wrapper.find(Header).prop('message')).toBe("Hello World");
});
setProps(newProps: any)
Sets the new props object of the root node. It can only be used on the root node.
it('should have an updated prop "message" with a value of "What the fun"', () => {
let wrapper = mount(<App />);
wrapper.setProps({ message: 'What the fun' })
expect(wrapper.find(Header).prop('message')).toBe("What the fun");
});
setState(newState : any, callbackFunc: Function)
Sets the new state object of the root node. It can only be used on the root node.
it('should have an updated prop "isDarkTheme" with a value of true', () => {
let wrapper = mount(<App />);
wrapper.setState({ isDarkTheme: true });
expect(wrapper.state('isDarkTheme')).toBeTruthy();
});
simulate(event:string, ...args)
Invokes an event on the current wrapper.
it('should have an updated value of "1234"', () => {
let wrapper = mount(<App />);
wrapper.find('input').simulate('change', { target: { value: '1234' } });
expect(wrapper.state('listItem')).toBe('1234');
});
state(key:string)
return the value of a state property.
it('should a input with a value of "abc"', () => {
let wrapper = shallow(<App />);
wrapper.setState({ listItem: 'abc' });
expect(wrapper.state('listItem')).toBe('abc');
});
it('should have an updated "message" prop with a value of "Hi"', () => {
let wrapper = mount(<App />);
wrapper.setProps({ message: 'Hi' });
expect(wrapper.prop('message')).toBe('Hi');
})
text()
returns the text of the current wrapper.
it('should a text of "Hello World"', () => {
let wrapper = mount(<App />);
expect(wrapper.find('h1').text()).toBe('Hello World');
});
type()
returns the type of the current wrapper.
it('should return the App', () => {
let wrapper = shallow(<App />);
expect(wrapper.at(0).type()).toBe('div');
});
it('should return the App', () => {
let wrapper = mount(<App />);
expect(wrapper.at(0).type()).toBe(App);
});
Check this post out to make your own Cover Image for your dev.to Post.
Thanks guys for reading this post.
Have a Nice Day 😃!.
Posted on August 28, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.