Learning to Test React Apps — Jest, React Testing Library and Enzyme

Charles Wisoff
5 min readNov 19, 2020

I’ve spent the last couple of weeks learning to write tests for React. There is A LOT to learn, and I haven’t found a good one stop shop that pulls all the pieces together. I will probably not get to that here, but I hope to provide some insights into how the different pieces fit together and how one might wrap their heads around React testing.

I’ve been writing out tests for one of my React projects, EduSource, so you can check out the code here for reference. I don’t have complete test coverage, but any component/file with the following structure has tests written for it:

– src
– components
– componentName
– __tests__
– componentName.test.js
– componentName.js

This is a pattern for structuring files that I found on a YouTube video tutorial for writing React tests.

Jest, Enzyme and React Testing Library

There are several tools and packages you can use to test a React app. React has it’s own test utilities that come with create-react-app. There are also packages like react-mock-store that help with testing Redux reducers and actions. The most popular tools, however, are Jest, Enzyme and React Testing Library.

Jest — jest is a general JavaScript Testing Framework. It can be used for React as well as Angular, Vue, or plain old JavaScript. It sets the foundations for React testing by allowing you to test:

Matching — matching, more or less, refers to comparing input to expected output. Any time you see expect([actual output]).toMatch([expected output]), a matcher of some sort is being used. This can be matching a value: expect([actual value]).toBe([expected value]) or a “matching” of the execution of a function: expect([jest.fn()]).toBeCalled(). The full API of matchers is here, and additional matchers, like toHaveTextContent() can be found through the package @testing-library/jest-dom/extend-expect .

Mocking & Spying — Often, you will want to test that a function has been called. This is a good way to isolate unit tests. If you simply test that one function calls another, you are not making any assumptions about the other function being called. Testing that a function is called can be done two ways:

(1) Mocking — In the code in the Matching section above, you see `jest.fn()`. This code creates a “mock” function that you can pass in as a prop instead of, for example, the function the parent component passes down. This might look like look like the following:

ParentComponent.jsconst changeState = e => {
e.preventDefault()
this.setState({ stateKey: e.target.value })
}
render() {
return <
ChildComponent changeState={this.props.changeState}/>
}
ChildComponent.jsrender() {
return <button data-testid="button" onClick={e => props.changeState(e)}>Submit</button>
}
ChildComponent.test.jsit ('calls the change state function when button is clicked', ()=>{
const mockChangeState = jest.fn()
render(<ChildComponent changeState={mockChangeState} />)
fireEvent.click(getTestById(‘button’))
expect(mockChangeState).toBeCalled()
)}

In addition to testing that a mock function has been called, you can test that it’s been called with specific arguments (see matchers api for additional test options) or you can create a mockImplementation of a function that simulates what a real function would do.

(2) Spying — another method to test the execution of a function is spying. Spying is used if you want to test matching behavior of an actual function instead of passing in a mock function. Take for example the parent component above:

ParentComponent.jsconst changeState = e => {
e.preventDefault()
this.setState({ stateKey: e.target.value })
}
render() {
return <
ChildComponent changeState={this.props.changeState}/>
}```

Instead of creating a mock changeState function, you could alternatively spy on the real function like so:

import ParentComponent from ‘./[Parent Component file path]’...const spy = jest.spyOn(ParentComponent, ‘changeState’)

Here, spy would replace mockChangeState. The first argument of jest.spyOn is an object and the second argument is an attribute of that object that has a function as a value.

To be honest, I had trouble finding a good use case for using jest.spyOn instead of creating a mock function. I was hopeful that I could use this to spy on functions that triggered Redux/Thunk dispatches and async api calls, but this didn’t work.

Snapshot Testing — Snapshot testing is sort of like an automated version of writing a bunch of matching tests. Snapshot testing utilizes both the package react-test-renderer as well as the toMatchSnapshot() jest matcher.

import { renderer } from ‘react-test-rendererit ('matches the snapshot', () => {
const tree = renderer.create(<Component {…mockProps} />).toJSON()
expect(tree).toMatchSnapshot()
}

The code above uses the renderer method to create a ‘snapshot’ of your component given a set of inputs (`mockProps` in this case). When the toMatchSnapshot() function is first run, it generates a snapshot folder and file that is a JSON representation of the HTML your component renders. Every time it is run afterward, it checks to see if there are any changes in the generated HTML. So, if my component generates <div>{user.name}</div> and I change that to <span>{user.name}></span> or <div>{user.email}</div>, the toMatchSnapshot() test will fail.

However, the nice thing about using snapshots is that it will print out a diff of the changes that have been made and give you the option to automatically update the snapshot to reflect those changes. Unlike using a basic matching test to test your UI, you can test it all at once and automatically update the snapshot instead of having to manually re-write a bunch of tests when the UI does change.

React Testing Library and Enzyme — I haven’t done extensive research, but as far as I can tell, React Testing Library and Enzyme serve the same purpose. Most of the things I was able to do with React Testing Library I was able to do with Enzyme and vice versa. Some of those things are easier or more janky depending on which library you pick. Broadly speaking, I used each to:

  • Render components and Find DOM elements (nodes)
  • Simulate/Fire Events

Render components and Find DOM elements (nodes) — When Enzyme and React Testing Library ‘render’ a component they simulate the ReactDOM.render() which renders your actual code. With React Testing Library you render components with the render method. For example:

Component.test.jsimport { render } from ‘@testing-library/react’...const { getByTestId } = render(<Component {…mockProps} />)
expect(getByTestId(‘idValue’)).toHaveTextContent(‘[what you expect to be displayed]’)

Here, the render function returns an object with several queries you can run against the rendered component. In this case, we use destructuring assignment to pull out the getByTestIdfunction (a list of these queries and what they return can be found here). getByTestId allows you to find an HTML element that has been rendered with the data-testid attribute set to whatever value you pass into getByTestId().

With Enzyme, rendering is done with either the shallow or mount functions, and you would find a DOM element with .find(). The equivalent of the code above would be:

import { shallow } from ‘enzyme’...const wrapper = shallow(<Component {…mockProps} />)
expect(wrapper.find(“[data-testid] = ‘idValue’”).toHaveTextContent(‘[what you expect to be displayed]’)

Simulate/Fire Events — One of the other basic things these libraries allow you to do is simulate user interaction with DOM nodes. The basic pattern in that you first find a node that has some DOM event associated with it, then trigger that event, and last check to see if the expected change occurred. For example with react-testing-library this might look like:

const { getByTestId } = render(<Component {…mockProps} />)
expect(getByTestId(‘button’).value).toBe(‘submit’)
fireEvent.click(getTestById(‘button’))
expect(getByTestId(‘button’).value).toBe(‘saved’)

Conclusion

I initially intended to include some other topics in this blog post, like testing Routing and testing components that utilize Redux, but I think it’s best to save those topics for other posts, as there is plenty of content here. I will link to those future posts here, once I’ve written them.

--

--

Charles Wisoff

Recent graduate of the Flatiron Software Engineering program. Former startup CEO and product manager. Writing about tech.