This is the second part of the series on Testing Components in React. If you have prior experience with Jest, you can skip ahead and use the GitHub code as a starting point.
In the previous article, we covered the basic principles and ideas behind test-driven development. We also set up the environment and the tools required for running tests in React. The toolset included Jest, ReactTestUtils, Enzyme, and react-test-renderer.
We then wrote a couple of tests for a demo application using ReactTestUtils and discovered its shortcomings compared to a more robust library like Enzyme.
In this post, we’ll get a deeper understanding of testing components in React by writing more practical and realistic tests. You can head to GitHub and clone my repo before getting started.
Getting Started With the Enzyme API
Enzyme.js is an open-source library maintained by Airbnb, and it’s a great resource for React developers. It uses the ReactTestUtils API underneath, but unlike ReactTestUtils, Enzyme offers a high-level API and easy-to-understand syntax. Install Enzyme if you haven’t already.
The Enzyme API exports three types of rendering options:
- shallow rendering
- full DOM rendering
- static rendering
Shallow rendering is used to render a particular component in isolation. The child components won’t be rendered, and hence you won’t be able to assert their behavior. If you’re going to focus on unit tests, you’ll love this. You can shallow render a component like this:
import { shallow } from 'enzyme'; import ProductHeader from './ProductHeader'; // More concrete example below. const component = shallow(<ProductHeader/>);
Full DOM rendering generates a virtual DOM of the component with the help of a library called jsdom. You can avail this feature by replacing the shallow()
method with mount()
in the above example. The obvious benefit is that you can render the child components also. If you want to test the behavior of a component with its children, you should be using this.
Static rendering is used to render react components to static HTML. It’s implemented using a library called Cheerio, and you can read more about it in the docs.
Revisiting Our Previous Tests
Here are the tests that we wrote in the last tutorial:
src/components/__tests__/ProductHeader.test.js
import ReactTestUtils from 'react-dom/test-utils'; // ES6 describe('ProductHeader Component', () => { it('has an h2 tag', () => { const component = ReactTestUtils .renderIntoDocument(<ProductHeader/>); var node = ReactTestUtils .findRenderedDOMComponentWithTag( component, 'h2' ); }); it('has a title class', () => { const component = ReactTestUtils .renderIntoDocument(<ProductHeader/>); var node = ReactTestUtils .findRenderedDOMComponentWithClass( component, 'title' ); }) })
The first test checks whether the ProducerHeader
component has an <h2>
tag, and the second one finds whether it has a CSS class named title
. The code is hard to read and understand.
Here are the tests rewritten using Enzyme.
src/components/__tests__/ProductHeader.test.js
import { shallow } from 'enzyme' describe('ProductHeader Component', () => { it('has an h2 tag', () => { const component = shallow(<ProductHeader/>); var node = component.find('h2'); expect(node.length).toEqual(1); }); it('has a title class', () => { const component = shallow(<ProductHeader/>); var node = component.find('h2'); expect(node.hasClass('title')).toBeTruthy(); }) })
First, I created a shallow-rendered DOM of the <ProductHeader/>
component using shallow()
and stored it in a variable. Then, I used the .find()
method to find a node with tag ‘h2’. It queries the DOM to see if there’s a match. Since there is only one instance of the node, we can safely assume that node.length
will be equal to 1.
The second test is very similar to the first one. The hasClass('title')
method returns whether the current node has a className
prop with value ‘title’. We can verify the truthfulness using toBeTruthy()
.
Run the tests using yarn test
, and both the tests should pass.
Well done! Now it’s time to refactor the code. This is important from a tester’s perspective because readable tests are easier to maintain. In the above tests, the first two lines are identical for both the tests. You can refactor them by using a beforeEach()
function. As the name suggests, the beforeEach
function gets called once before each spec in a describe block is executed.
You can pass an arrow function to beforeEach()
like this.
src/components/__tests__/ProductHeader.test.js
import { shallow } from 'enzyme' describe('ProductHeader Component', () => { let component, node; // Jest beforeEach() beforeEach((()=> component = shallow(<ProductHeader/>) )) beforeEach((()=> node = component.find('h2')) ) it('has an h2 tag', () => { expect(node).toBeTruthy() }); it('has a title class', () => { expect(node.hasClass('title')).toBeTruthy() }) })
Writing Unit Tests With Jest and Enzyme
Let’s write a few unit tests for the ProductDetails component. It is a presentational component that displays the details of each individual product.
The unit test will try to assert the following assumptions:
- The component exists and the props are getting passed down.
- The props like product’s name, description, and availability are displayed.
- An error message is displayed when the props are empty.
Here is the bare-bones structure of the test. The first beforeEach()
stores the product data in a variable, and the second one mounts the component.
src/components/__tests__/ProductDetails.test.js
describe("ProductDetails component", () => { var component, product; beforeEach(()=> { product = { id: 1, name: 'NIKE Liteforce Blue Sneakers', description: 'Lorem ipsum.', status: 'Available' }; }) beforeEach(()=> { component = mount(<ProductDetails product={product} foo={10}/>); }) it('test #1' ,() => { }) })
The first test is easy:
it('should exist' ,() => { expect(component).toBeTruthy(); expect(component.props().product).toEqual(product); })
Here we use the props()
method which is handy for getting the props of a component.
For the second test, you can query elements by their class names and then check whether the product’s name, description etc. are part of that element’s innerText
.
it('should display product data when props are passed', ()=> { let title = component.find('.product-title'); expect(title.text()).toEqual(product.name); let description = component.find('.product-description'); expect(description.text()).toEqual(product.description); })
The text()
method is particularly useful in this case to retrieve the inner text of an element. Try writing an expectation for the product.status()
and see if all the tests are passing.
For the final test, we’re going to mount the ProductDetails
component without any props. Then we’re going to look for a class named ‘.product-error’ and check if it contains the text “Sorry, Product doesn’t exist”.
it('should display an error when props are not passed', ()=> { /* component without props */ component = mount(<ProductDetails />); let node = component.find('.product-error'); expect(node.text()).toEqual('Sorry. Product doesnt exist'); })
That’s it. We’ve successfully tested the <ProductDetails />
component in isolation. Tests of this type are known as unit tests.
Testing Callbacks Using Stubs and Spies
We just learned how to test props. But to truly test a component in isolation, you also need to test the callback functions. In this section, we’ll write tests for the ProductList component and create stubs for callback functions along the way. Here are the assumptions that we need to assert.
- The number of products listed should be equivalent to the number of objects the component receives as props.
- Clicking on
<a>
should invoke the callback function.
Let’s create a beforeEach()
function that fills in mock product data for our tests.
src/components/__tests__/ProductList.test.js
beforeEach( () => { productData = [ { id: 1, name: 'NIKE Liteforce Blue Sneakers', description: 'Lorem ipsu.', status: 'Available' }, // Omitted for brevity ] })
Now, let’s mount our component in another beforeEach()
block.
beforeEach(()=> { handleProductClick = jest.fn(); component = mount( <ProductList products = {productData} selectProduct={handleProductClick} /> ); })
The ProductList
receives the product data through props. In addition to that, it receives a callback from the parent. Although you could write tests for the parent’s callback function, that’s not a great idea if your aim is to stick to unit tests. Since the callback function belongs to the parent component, incorporating the parent’s logic will make the tests complicated. Instead, we are going to create a stub function.
What’s a Stub?
A stub is a dummy function that pretends to be some other function. This allows you to independently test a component without importing either parent or child components. In the example above, we created a stub function called handleProductClick
by invoking jest.fn()
.
Now we just need to find the all the <a>
elements in the DOM and simulate a click on the first <a>
node. After being clicked, we’ll check if handleProductClick()
was invoked. If yes, it’s fair to say our logic is working as expected.
it('should call selectProduct when clicked', () => { const firstLink = component.find('a').first(); firstLink.simulate('click'); expect(handleProductClick.mock.calls.length).toEqual(1); }) })
Enzyme lets you easily simulate user actions such as clicks using simulate()
method. handlerProductClick.mock.calls.length
returns the number of times the mock function was called. We expect it to be equal to 1.
The other test is relatively easy. You can use the find()
method to retrieve all <a>
nodes in the DOM. The number of <a>
nodes should be equal to the length of the productData array that we created earlier.
it('should display all product items', () => { let links = component.find('a'); expect(links.length).toEqual(productData.length); })
Testing the Component’s State, LifeCycleHook, and Method
Next up, we’re going to test the ProductContainer
component. It has a state, a lifecycle hook, and a class method. Here are the assertions that need to be verified:
-
componentDidMount
is called exactly once. - The component’s state is populated after the component mounts.
- The
handleProductClick()
method should update the state when a product id is passed in as an argument.
To check whether componentDidMount
was called, we’re going to spy on it. Unlike a stub, a spy is used when you need to test an existing function. Once the spy is set, you can write assertions to confirm whether the function was called.
You can spy on a function as follows:
src/components/__tests__/ProductContainer.test.js
it('should call componentDidMount once', () => { componentDidMountSpy = spyOn(ProductContainer.prototype, 'componentDidMount'); //To be finished });
The first parameter to jest.spyOn
is an object that defines the prototype of the class that we’re spying on. The second one is the name of the method that we want to spy.
Now render the component and create an assertion to check whether spy was called.
component = shallow(<ProductContainer/>); expect(componentDidMountSpy).toHaveBeenCalledTimes(1);
To check that the component’s state is populated after the component mounts, we can use Enzyme’s state()
method to retrieve everything in the state.
it('should populate the state', () => { component = shallow(<ProductContainer/>); expect(component.state().productList.length) .toEqual(4) })
The third one is a bit tricky. We need to verify that handleProductClick
is working as expected. If you head over to the code, you’ll see that the handleProductClick()
method takes a product id as input, and then updates this.state.selectedProduct
with the details of that product.
To test this, we need to invoke the component’s method, and you can actually do that by calling component.instance().handleProductClick()
. We’ll pass in a sample product id. In the example below, we use the id of the first product. Then, we can test whether the state was updated to confirm that the assertion is true. Here’s the whole code:
it('should have a working method called handleProductClick', () => { let firstProduct = productData[0].id; component = shallow(<ProductContainer/>); component.instance().handleProductClick(firstProduct); expect(component.state().selectedProduct) .toEqual(productData[0]); })
We’ve written 10 tests, and if everything goes well, this is what you should see:
Summary
Phew! We’ve covered almost everything that you need to know to get started with writing tests in React using Jest and Enzyme. Now might be a good time to head over to the Enzyme website to have a deeper look at their API.
What are your thoughts on writing tests in React? I’d love to hear them in the comments.