Michael Hueter
Instructor at Rithm School and Full-Stack Software Developer
Jest ships with CRA (Create React App)
In any CRA project, just run "npm test" in the directory and jest will work
Jest looks for tests in two places:
Any file with .test or .spec in the name (e.g. App.test.js or App.spec.js)
Any file in src/__tests__ folder
Enzyme gives us the ability to "virtually render" components at whatever level we want
It includes jQuery-like selectors (more on that later)
Here's how you install it:
npm install --save-dev enzyme enzyme-adapter-react-16 \
react-test-renderer enzyme-to-json
Here's how you configure it:
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
new file => src/setupTests.js
Let's add tests to a React Meme Generator!
import React from 'react'; // for JSX
import { shallow } from 'enzyme'; // how to mount the component
import Meme from '.'; // import the component itself
describe('<Meme />', () => {
let wrapper;
it('renders', () => {
// smoke test!!!
wrapper = shallow(
<Meme
url="https://i.ytimg.com/vi/I7jgu-8scIA/maxresdefault.jpg"
topText="I can haz"
bottomText="react tests"
/>
);
});
});
Let's test the easiest component, "Meme.js"
npm test
terminal ->
src/Meme/index.test.js
shallow(<Component />) for Shallow rendering is useful to constrain yourself to testing a component as a unit, and to ensure that your tests aren't indirectly asserting on behavior of child components.
In other words, shallow just renders the component itself and no nested components or lifecycle methods. Also, it doesn't even put it on the virtual DOM.
import React from 'react'; // for JSX
import { shallow } from 'enzyme'; // how to mount the component
import App from '.';
describe('<App />', () => {
it('renders', () => {
let wrapper = shallow(<App />);
console.log(wrapper.debug());
});
});
<div className="App">
<NewMemeForm addMeme={[Function]} />
<hr />
</div>
class App extends Component {
render() {
return (
<div className="App">
<NewMemeForm addMeme={this.addMeme} />
<hr />
{this.state.memes.map(m => (
<Meme key={m.id} {...m} deleteMeme={this.deleteMeme} />
))}
</div>
);
}
}
mount
mount(<Component />) for Full DOM rendering is ideal for use cases where you have components that may interact with DOM apis, or may require the full lifecycle in order to fully test the component (ie, componentDidMount etc.)
mount is much more expensive than shallow, but you need to do it any time you want to test lifecycle methods or components with routers, etc.
import React from 'react'; // for JSX
import { mount } from 'enzyme'; // how to mount the component
import App from '.';
describe('<App />', () => {
it('renders', () => {
let wrapper = mount(<App />);
console.log(wrapper.debug());
});
});
<App>
<div className="App">
<NewMemeForm addMeme={[Function]}>
<div>
<h2>
Make a new Meme
</h2>
<form onSubmit={[Function]}>
<label htmlFor="url">
Image URL
</label>
<input type="text" name="url" id="form_url" onChange={[Function]} value="" />
<label htmlFor="topText">
Top Text
</label>
<input type="text" name="topText" id="form_topText" onChange={[Function]} value="" />
<label htmlFor="bottomText">
Bottom Text
</label>
<input type="text" name="bottomText" id="form_bottomText" onChange={[Function]} value="" />
<button type="submit" id="form_submit">
Generate Meme!
</button>
</form>
</div>
</NewMemeForm>
<hr />
</div>
</App>
instead of shallow
A snapshot test is a super easy, quick test to add that is a great improvement over a simple smoke test.
Jest compares the current snapshot with the saved snapshot, and throws a test error if they are different.
Basically, you save the configuration of a component in a snapshot, a file that gets placed in a __snapshots__ folder.
To make our snapshots super easy to read, we'll first use a package to convert our rendered component into JSON form.
import React from 'react'; // for JSX
import { shallow } from 'enzyme'; // how to mount the component
import toJson from 'enzyme-to-json';
import Meme from '.'; // import the component itself
describe('<Meme />', () => {
let wrapper;
it('renders', () => {
// smoke test!!!
wrapper = shallow(
<Meme
url="https://i.ytimg.com/vi/I7jgu-8scIA/maxresdefault.jpg"
topText="I can haz"
bottomText="react tests"
/>
);
});
it('matches snapshot', () => {
const serialized = toJson(wrapper);
});
});
import React from 'react'; // for JSX
import { shallow } from 'enzyme'; // how to mount the component
import toJson from 'enzyme-to-json';
import Meme from '.'; // import the component itself
describe('<Meme />', () => {
let wrapper;
it('renders', () => {
wrapper = shallow(
<Meme
url="https://i.ytimg.com/vi/I7jgu-8scIA/maxresdefault.jpg"
topText="I can haz"
bottomText="react tests"
/>
);
});
it('matches snapshot', () => {
const serialized = toJson(wrapper);
expect(serialized).toMatchSnapshot();
});
});
Finally, Jest has a specific assertion to take a snapshot:
npm test
terminal ->
import React from 'react';
import './style.css';
function Meme(props) {
return (
<div id="memez">
<button onClick={() => props.deleteMeme(props.id)}>DELETE</button>
<span>{props.topText}</span>
<img src={props.url} alt="a meme" />
<span>{props.bottomText}</span>
</div>
);
}
export default Meme;
src/Meme/index.js
Let's add an ID to the Meme div
npm test
terminal ->
npm test
terminal ->
u
When you need to test something business-critical, sometimes snapshots are not enough.
Let's do a basic example with the Meme component to use the mounted wrapper's .props() method to get at an actual value:
import React from 'react'; // for JSX
import { mount } from 'enzyme'; // how to mount the component
import toJson from 'enzyme-to-json'; // for snapshotting the component
import Meme from '.'; // import the component itself
describe('<Meme />', () => {
it('has correct topText prop', () => {
let wrapper = mount(
<Meme
url="https://i.ytimg.com/vi/I7jgu-8scIA/maxresdefault.jpg"
topText="I can haz"
bottomText="react tests"
/>
);
expect(wrapper.props().topText).toBe('I can haz');
});
});
Even fancier are enzyme selectors, which act similar to jQuery.
describe('<Meme />', () => {
it('applies url prop to image src', () => {
let wrapper = mount(
<Meme
url="https://i.ytimg.com/vi/I7jgu-8scIA/maxresdefault.jpg"
topText="I can haz"
bottomText="react tests"
/>
);
let img = wrapper.find('img'); // select the img tag
// assert that we are passing src to the img
expect(img.prop('src')).toBe(
'https://i.ytimg.com/vi/I7jgu-8scIA/maxresdefault.jpg'
);
});
});
When you need to assert state changes, you can simulate events. Let's test the NewMemeForm component!
describe('<NewMemeForm />', () => {
it('updates state with topText input', () => {
// mount form and mock addMeme
let wrapper = mount(<NewMemeForm addMeme={jest.fn()} />);
// assert empty string default
expect(wrapper.state().topText).toBe('');
// select form input
let form_topText = wrapper.find('#form_topText');
// put 'xyz' as the value into the form input
form_topText.instance().value = 'xyz';
// send a "change" event
form_topText.simulate('change');
// assert that state has updated
expect(wrapper.state().topText).toBe('xyz');
});
});
Takeaways
Components with anything from react-router are harder to test than pure components.
This is because if you mount a component with a reference to React router (a <Route />, or <Link /> for instance), it requires the context of the original router in the test.
Let's add tests to Matt's awesome vending machine app:
Let's try to test <Sardines />, which has a <Link /> in it.
import React from 'react';
import { Link } from 'react-router-dom';
import Message from './Message';
import './Sardines.css';
const Sardines = props => (
<div
className="Sardines"
style={{
backgroundImage:
'url(https://media.giphy.com/media/tVk4w6EZ7eGNq/giphy.gif)'
}}
>
<Message>
<h1>you don't eat the sardines. the sardines, they eat you!</h1>
<h1>
<Link to="/">go back</Link>
</h1>
</Message>
</div>
);
export default Sardines;
Let's try to test <Sardines />, which has a <Link /> in it.
import React from 'react'; // for JSX
import { shallow } from 'enzyme'; // how to mount the component
import toJson from 'enzyme-to-json'; // for snapshotting the component
import Sardines from '../Sardines'; // import the component itself
describe('<Sardines />', () => {
let wrapper;
it('renders', () => {
wrapper = shallow(<Sardines />);
});
it('matches snapshot', () => {
expect(toJson(wrapper)).toMatchSnapshot();
});
});
Shallow mounting worked!
import React from 'react'; // for JSX
import { mount } from 'enzyme'; // how to mount the component
import toJson from 'enzyme-to-json'; // for snapshotting the component
import Sardines from '../Sardines'; // import the component itself
describe('<Sardines />', () => {
let wrapper;
it('renders', () => {
wrapper = mount(<Sardines />);
});
it('matches snapshot', () => {
expect(toJson(wrapper)).toMatchSnapshot();
});
});
Uh oh...
A MemoryRouter is <Router> that keeps the history of your “URL” in memory (does not read or write to the address bar). Useful in tests and non-browser environments like React Native.
Unfortunately, if we want to fully mount Sardines, we need to wrap it in a MemoryRouter.
import React from 'react'; // for JSX
import { mount } from 'enzyme'; // how to mount the component
import { MemoryRouter } from 'react-router-dom';
import toJson from 'enzyme-to-json'; // for snapshotting the component
import Sardines from '../Sardines'; // import the component itself
describe('<Sardines />', () => {
let wrapper;
it('renders', () => {
wrapper = mount(
<MemoryRouter>
<Sardines />
</MemoryRouter>
);
});
it('matches snapshot', () => {
expect(toJson(wrapper)).toMatchSnapshot();
});
});
Update snapshot and tests pass!!!
Unfortunately, if you run the tests again, you get a snapshot fail.
describe('<Sardines />', () => {
let wrapper;
it('renders', () => {
wrapper = mount(
<MemoryRouter initialEntries={[{ key: 'testKey' }]}>
<Sardines />
</MemoryRouter>
);
});
it('matches snapshot', () => {
expect(toJson(wrapper)).toMatchSnapshot();
});
});
Prevent the "key" from being randomly re-generated each time.
Takeaways
By Michael Hueter
Learn how to test React components with Jest and Enzyme