- 1. JavaScript testing #1. Explaining types of tests. Basics of unit testing with Jest
- 2. JavaScript testing #2. Introducing Enzyme and testing React components
- 3. JavaScript testing #3. Testing props, the mount function and snapshot tests.
- 4. JavaScript testing #4. Mocking API calls and simulating React components interactions
- 5. JavaScript testing #5. Testing hooks with react-hooks-testing-library and Redux
- 6. JavaScript testing #6. Introduction to End-to-End testing with Cypress
- 7. JavaScript testing #7. Diving deeper into commands and selectors in Cypress
- 8. JavaScript testing #8. Integrating Cypress with Cucumber and Gherkin
- 9. JavaScript testing #9. Replacing Enzyme with React Testing Library
- 10. JavaScript testing #10. Advanced mocking with Jest and React Testing Library
- 11. JavaScript testing #11. Spying on functions. Pitfalls of not resetting Jest mocks
- 12. JavaScript testing #12. Testing downloaded files and file inputs with Cypress
- 13. JavaScript testing #13. Mocking a REST API with the Mock Service Worker
- 14. JavaScript testing #14. Mocking WebSockets using the mock-socket library
- 15. JavaScript testing #15. Interpreting the code coverage metric
- 16. JavaScript testing #16. Snapshot testing with React, Jest, and Vitest
- 17. JavaScript testing #17. Introduction to End-to-End testing with Playwright
- 18. JavaScript testing #18. E2E Playwright tests for uploading and downloading files
A few days ago, React released version 18, which is not compatible with Enzyme. Furthermore, it probably is not achievable to use Enzyme with React 18. If you’re still using Enzyme, it is time to look into alternatives. The most popular option besides Enzyme seems to be React Testing Library. However, incorporating RTL requires a slightly different testing approach and is not a drop-in replacement for Enzyme. This article goes through some of the most crucial aspects of testing and compares Enzyme and React Testing Library.
Testing an example application with both approaches
The React Testing Library aims to provide a lightweight solution for testing React components. Its most fundamental principle is to do testing in a way that resembles how our component would be used in a real application. RTL gives us the tool to interact with the component in a way that the user would.
Defining a simple counter application
To better grasp the idea, let’s create a very straightforward application. Our Counter component displays a button and a label.
Counter.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import React from 'react'; import useCounter from './useCounter'; import CounterLabel from './CounterLabel'; const Counter = () => { const { counterNumber, handleButtonClick } = useCounter(); return ( <div> <button onClick={handleButtonClick}>Click</button> <CounterLabel counterNumber={counterNumber} /> </div> ); }; export default Counter; |
CounterLabel.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import React, { FunctionComponent } from 'react'; interface Props { counterNumber: number; } const CounterLabel: FunctionComponent<Props> = ({ counterNumber }) => { return ( <p>The number of clicks: {counterNumber}</p> ); }; export default CounterLabel; |
When we click on the button, the label should display a bigger number.
useCounter.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { useState } from 'react'; function useCounter() { const [counterNumber, setCounterNumber] = useState(0); const handleButtonClick = () => { setCounterNumber(counterNumber + 1); }; return { counterNumber, handleButtonClick, }; } export default useCounter; |
Testing with React Testing Library
Let’s test if the Counter component displays the correct number before and after clicking a button.
Counter.test.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import { render, fireEvent } from '@testing-library/react'; import Counter from './Counter'; describe('The Counter component', () => { describe('if the button is not clicked', () => { it('should display 0', () => { const counter = render(<Counter />); const paragraph = counter.getByText('The number of clicks: 0', { selector: 'p', }); expect(paragraph).toBeDefined(); }); }); describe('if the button clicked once', () => { it('should display 1', () => { const counter = render(<Counter />); const button = counter.getByText('Click', { selector: 'button' }); fireEvent.click(button); const paragraph = counter.getByText('The number of clicks: 1', { selector: 'p', }); expect(paragraph).toBeDefined(); }); }); }); |
PASS src/Counter/Counter.test.tsx
The Counter component
if the button is not clicked
✓ should display 0
if the button clicked once
✓ should display 1
In the above test, we check the simulated DOM to see if React rendered the correct number before and after we’ve clicked the button.
Testing with Enzyme
We can use a different testing approach if we want to test the above component with Enzyme.
Counter.test.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import { shallow } from 'enzyme'; import Counter from './Counter'; import CounterLabel from './CounterLabel'; describe('The Counter component', () => { describe('if the button is not clicked', () => { it('should render CounterLabel with 0', () => { const counter = shallow(<Counter />); const counterLabel = counter.find(CounterLabel); expect(counterLabel.prop('counterNumber')).toBe(0); }); }); describe('if the button clicked once', () => { it('should render CounterLabel with 1', () => { const counter = shallow(<Counter />); const increaseCounterNumber = button.prop('onClick'); increaseCounterNumber(); const counterLabel = counter.find(CounterLabel); expect(counterLabel.prop('counterNumber')).toBe(1); }); }); }); |
PASS src/Counter/Counter.test.tsx
The Counter component
if the button is not clicked
✓ should render CounterLabel with 0
if the button clicked once
✓ should render CounterLabel with 1
We don’t check the simulated DOM in the above test to see what number React rendered. Instead, we check if the Counter component rendered the CounterLabel with the right prop.
Comparing the React Testing Library and Enzyme
In the above test, React Testing Library forces us to write tests that resemble the way that a real user acts. In contrast, Enzyme allows us to test the implementation details of a component more deeply.
Integration tests and unit tests
We can perceive the test we wrote with Enzyme as a unit test. We only test the Counter component and don’t care about the CounterLabel. Even if the CounterLabel stops displaying the label, our test won’t fail.
We can call the test we wrote with React Testing Library an integration test. We verify if the Counter component works well with the CounterLabel to display the right message to the user. If the CounterLabel stops working correctly, the test we wrote with RTL will fail.
Both of the above approaches have their consequences. So let’s modify our component a little and see if the tests still pass.
CounterLabel.tsx
1 2 3 4 5 6 7 8 9 10 11 |
import React, { FunctionComponent } from 'react'; interface Props { counterNumber: number; } const CounterLabel: FunctionComponent<Props> = ({ counterNumber }) => { return <p>The number of clicks: {counterNumber * 100}</p>; }; export default CounterLabel; |
Our CounterLabel component no longer displays the counter correctly. Because of that, the test we wrote with React Testing Library does not pass anymore. Unfortunately, the test we wrote in Enzyme did not fail. Inside, we only check if the Counter component passes the counterNumber correctly to the CounterLabel.
Let’s modify our component differently, not necessarily causing a bug.
Counter.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import React from 'react'; import useCounter from './useCounter'; const Counter = () => { const { counterNumber, handleButtonClick } = useCounter(); return ( <div> <button onClick={handleButtonClick}>Click</button> <p>The number of clicks: {counterNumber}</p>; </div> ); }; export default Counter; |
Above, we’re not using the CounterLabel component anymore. Instead, we render the counterNumber directly in the Counter component.
From the user’s point of view, nothing changed. Unfortunately, the tests we wrote using Enzyme fail now. There, we wrote the following line of code:
1 |
const counterLabel = counter.find(CounterLabel); |
Since we no longer use the CounterLabel component, the tests written with Enzyme don’t pass. However, the tests we wrote with React Testing Library still passed without issues. They don’t care whether or not we use the CounterLabel component.
Shallow and full rendering
In our Enzyme test, we use shallow rendering. It does not render the child components. Therefore, it emphasizes unit testing.
It’s worth noticing that we don’t have to use shallow rendering with Enzyme. Instead, we can use the mount function. It will render both Counter and CounterLabel, allowing us to write an integration test.
Counter..test.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import { mount } from 'enzyme'; import Counter from './Counter'; describe('The Counter component', () => { describe('if the button is not clicked', () => { it('should display 0', () => { const counter = mount(<Counter />); const paragraph = counter.find('p'); expect(paragraph.text()).toBe('The number of clicks: 0'); }); }); describe('if the button clicked once', () => { it('should display 1', () => { const counter = mount(<Counter />); const button = counter.find('button'); button.simulate('click'); const paragraph = counter.find('p'); expect(paragraph.text()).toBe('The number of clicks: 1'); }); }); }); |
The above test we wrote with Enzyme behaves similarly to the test we previously wrote with React Testing Library. However, it does not care much about the implementation details of the Counter component. Therefore, it allows us to modify it to some extent without the test failing.
In React Testing Library, the shallow rendering is not available. Although we could mock the components we don’t want to render, the official FAQ discourages that.
Summary
At some point, React Testing Library became more widely used than Enzyme. Since Enzyme can’t work with React 18, which was recently released, it will probably cause RTL to surpass Enzyme even more.
In this article, we’ve gone through their most apparent differences and how it changes how we write tests. The crucial thing is that the React Testing Library is not a straightforward replacement for Enzyme. Both React Testing Library and Enzyme are good libraries, and they share a set of functionalities.
Enzyme gives us more flexibility in the way that we write tests. React Testing Library, on the other hand, is more opinionated. It forces us to have a specific mindset and focus on what our user experiences. Instead of testing the implementation details, we have to simulate how a user would interact with our components. It might force us to shift our approach if we want to switch to React Testing Library. We might end up with tests that follow more best practices. It could also improve our confidence in our code and make sure that the application works well as a whole.