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.
Top comments (4)
Hey Mark!
First of all, congratulations on the great article and for sharing your knowledge. I encourage automated tests as specification, documentation and quality assurance for any software. I love writing tests and I respect A LOT people who see the value of good tests.
Seeing your article, I couldn't help but comment some of my own learnings on tests.
I've had some very painful experiences with testing that led to learnings I'll never forget.
When I started getting comfortable with writing tests, my tests looked pretty much like those you write. They seemed like very good tests and I loved seeing them all pass in my CI server. With every new push, more and more joy of seeing those tests pass.
Until I had to refactor.
Think about a colleague of yours looking at this spec in a few weeks from now:
'should have a child component of type "Header" at "index" of 0'
.Let's say this colleague has to add a new feature, which is a breadcrumb that stays above the header. As soon as he implements this, this test WILL break and it won't say anything useful.
The header is still there, in the position your users expect it to be, serving the function it should be serving. But your relentless CI Server WILL reprove this commit and your colleague is going to have to change this spec just to make it pass.
Imagine it happening every single moment in every single change in the codebase with many many tests breaking, almost like the codebase was screaming. It's chaos.
Now, if you think about it, this spec doesn't say anything about your component. If you read it to the stakeholders they won't have a clue if that's right or not. You won't be able to use this spec to communicate with anyone involved with this software, except Enzyme itself.
There are several good ways of writing frontend tests that assert and specify something meaningful and very useful for the software maintenance and lifetime.
In your case, I'd start by reading some very nice stuff from Kent Dodds and taking a look at @testing-library/react.
I hope you find my comment useful and I'm eager to see you evolve in your engineering journey :)
Hi Pedro! I always find it hard what to write inside the 😃
it('what to write here')
, do I use the method name of the method I'm gonna test? do I have to make the sentence longer so my teammates can understand or do I have to be more specific and be more technical?But after reading your comment I finally understand that writing tests should be readable and should be easily understandable to another person specifically a teammate. I really appreciate your comment it helped me realize that writing tests and code should be the same they must be readable and should be easy so people can understand it.
I'm gonna try that library today. Thanks, man! Have a great day.
Great, Mark!
I see you have great coding skills. It'll be very easy for you to get even better at writing good tests.
Hi -
Thanks for putting this tutorial together! One thing I noticed, however: the tests won't run (at least not on my Mac) unless the react-dom dependency is installed--
npm i -S react-dom
--and an index.js file is created at the top level of the directory (something like the image cap below):
dev-to-uploads.s3.amazonaws.com/i/...
Otherwise, great work!