Testing React components connected to a Redux store can be a challenging task that raises a few questions. What's the best way to ensure the component works as expected? Should you mock the store or use the real one? What about API requests?
In this post, I'll share my conclusion after asking myself these questions and what you need to do to test your Redux-connected React components.
Setup
In this tutorial, we'll use the following tools for testing.
Both of these libraries are included in Create-React-App (CRA). If you don't use CRA, you can install the necessary packages with the following commands:
# Install Testing library
yarn add @testing-library/jest-dom @testing-library/dom @testing-library/react @testing-library/user-event
# Install Jest
yarn add jest @types/jest
In the setupTests.ts
file, we add the following import to extend Jest's expect
function.
This will allow us to make assertions over the content of the DOM, like for example expect(element).toBeInTheDocument()
or expect(element).toHaveTextContent(/test/i)
:
import '@testing-library/jest-dom/extend-expect';
For further information on setting up Jest, I'd recommend that you check out their documentation.
Why mocking the Redux store isn't a good idea
When I tried to write tests for Redux-connected components, my first intuition was to mock all Redux-related functions. After all, I wanted to write unit tests, right?
It turns out that this approach is not only not recommended but also quite a bit of work since I'd need to create the appropriate mocks for any Redux actions, selectors, and reducers that my components would use.
The reason why it's not recommended is that these tests will give us very little confidence in regards to whether my application works correctly or not.
Kent's article explains this more in detail. In short, the author advises against testing implementation details and recommends testing components without mocking any dependencies if possible.
These tests, which are, in fact, integration tests, will give us more confidence that our code works, not in isolation, but together with all its dependencies, just as we will use it in production.
How to test connected components
So after establishing that we want to mock as few parts as possible, we can now create our tests.
At first, I usually create a smoke test that simply renders the component. We don't want to mock the Redux store, so we need to wrap our component in a Provider
component:
import {render, screen} from "@testing-library/react";
import {Provider} from "react-redux";
import {createStore} from "../../app/store";
import {Counter} from "./Counter";
describe('Counter component', () => {
test('renders the counter component', () => {
render(
<Provider store={createStore()}>
<Counter />
</Provider>
);
expect(screen.getByText(/Add Amount/i)).toBeInTheDocument();
});
});
The createStore
function creates a new store every time we call it:
import { configureStore } from '@reduxjs/toolkit';
export const createStore = () => configureStore({
reducer: {
counter: counterReducer,
},
});
I will usually create a separate function to render the components so I don't need to re-type the render
function for each test:
describe('Counter component', () => {
const produceComponent = () => render( <Provider store={createStore()}> <Counter /> </Provider> );
test('renders the counter component', () => {
produceComponent();
expect(screen.getByText(/Add Amount/i)).toBeInTheDocument();
});
});
Adding more tests now is easy. To simulate user interactions, we use the user-event package of the testing library @testing-library/user-event
:
import userEvent from "@testing-library/user-event";
// ...
describe('Counter component', () => {
// ...
test('increments the counter', () => {
produceComponent();
userEvent.click(screen.getByLabelText(/increment value/i));
expect(screen.getByText('1')).toBeInTheDocument();
});
// ...
});
Mocking API requests
There's a critical part of our application that we do need to mock, though: our API requests.
Our integration tests call an API slows down their execution and makes them error-prone since something like a network issue or a backend failure will already cause them to fail.
By mocking these requests, we control what these requests are supposed to return each time and avoid making real API requests over the network.
Mocking API requests is worth an article on its own. Luckily, an excellent one on the topic already exists, which explains how to set up a mock service worker that mocks all your API requests with very little effort—I haven't tried this out yet, but it looks promising.
Conclusion
As you could hopefully see in this article, testing Redux-connected React components doesn't need to be hard.
Using our Redux store instead of mocking it makes our tests more reliable and easier to write.
What is your experience with testing React components? Do you use a different approach? Let me know in the comments!
Some articles and links that may interest you:
Are you new to unit testing or want to get an overview of unit testing in React? In this article, I explain what you need to know about unit testing as a React developer, including some of the tools you can choose from.
Separation of concerns with React hooks
In this article, you'll learn how to use custom React hooks to organize your code.
The author of this post found a better way to deal with API requests in unit tests: a mock service worker. This service worker will intercept all network requests without having to mock the API requests via Jest.