In my experience unit testing React components was always pretty simple until you needed to integrate with an external library like Redux. There are countless "solutions" available online, but I wanted to present you with my simple, yet powerful solution. I used it in a few commercial projects with great results.
It couldn't be easier without React Testing Library, which revolutionized testing components in React.
A common mistake with testing Redux connected components
One solution which was, for some reason, quite popular was testing components connected to Redux without the real connection. Here is an example of what I mean by that.
import React from "react";
import { connect } from "react-redux";
const MyComponent = () => { ... };
const mapStateToProps = state => ({
data: state.someData,
});
export { MyComponent as MyComponentUnwrapped };
export default connect(mapStateToProps)(MyComponent);
And then in tests, you would import MyComponentUnwrapped
instead of your default export used everywhere else in the app.
In this case, you are not testing an important part of your component. Other than that MyComponentUnwrapped
is used only by your tests, your real application uses the default export. You can get your tests passing where in reality the same cases could fail.
How to test components in React Testing Library
React Testing Library provides a very intuitive API. Its main goal is to test components the same way user will use them in your application. Of course, the same is possible with other testing libraries like Enzyme, but React Testing Library is very strict about it and doesn't allow to access the internals of your component.
Enough with theory. Let's write some tests!
Let's say we have some components, which fetch user data and display it.
import React, { useState, useEffect } from "react";
import { getUserData } from "./api";
const User = () => {
const [userData, setUserData] = useState(null);
useEffect(() => {
getUserData.then((data) => {
setUserData(data);
});
});
if (!userData) {
return null;
}
return <div>{userData.name}</div>;
};
export default User;
Quite simple component, Now let's see how it can be tested
import React from "react";
import { screen, render } from "@testing-library/react";
import User from "./User";
jest.mock("./api", () => ({
getUserData: () => ({ name: "mock name" })
}));
describe("User", () => {
it("should display user name", async () => {
render(<User />);
const userName = await screen.findByText("mock name");
expect(userName).toBeTruthy();
});
});
The first thing we have to do is mock our API call with jest.mock
. Normally it would make a network request, but in tests, we need to mock it.
Then we use render
function to render our component, and screen.findByText
to search for text in the component we just rendered.
Testing Redux connected components
Now let's suppose we would need to access user data in other parts of the application. Let's move it to the Redux store. Here is how the refactored version of the component might look like.
import React, { useState, useEffect } from "react";
import { connect } from "react-redux";
import { fetchUserData } from './actions';
const User = ({ userData, fetchUserData }) => {
useEffect(() => {
fetchUserData();
}, []);
if (!userData) {
return null;
}
return <div>{userData.name}</div>;
};
const mapStateToProps = (state) => ({
userData: state.user
});
const mapDispatchToProps = {
fetchUserData,
};
export default connect(mapStateToProps, mapDispatchToProps)(User);
Now the first thing you will notice in your test is: Could not find "store" in the context of "Connect(User)"
error. It's because your component needs to be wrapper in Provider
to access Redux Store. Let's fix our tests:
import React from "react";
import { screen, render } from "@testing-library/react";
import { createStore } from "redux";
import User from "./User";
import reducer from "./reducer";
import store from "./store";
jest.mock("./api", () => ({
getUserData: () => ({ name: "mock name" })
}));
const initialState = {
user: { name: "mock name" },
};
const store = createStore(reducer, initialState);
const Wrapper = ({ children }) => (
<Provider store={store}>{children}</Provider>
);
describe("User", () => {
it("should display user name", async () => {
render(<User />, { wrapper: Wrapper });
const userName = await screen.findByText("mock name");
expect(userName).toBeTruthy();
});
});
We fixed the error by creating a Wrapper. This component will wrap the component we test with Provider and apply a mocked state. We can go one step further by custom render function using the one from React Testing Library.
import React from "react";
import { render as rtlRender } from "@testing-library/react";
import { createStore } from "redux";
import { Provider } from "react-redux";
import reducer from "./reducer";
export const renderWithState = (
ui,
{ initialState, ...renderOptions } = {}
) => {
const store = createStore(reducer, initialState);
const Wrapper = ({ children }) => (
<Provider store={store}>{children}</Provider>
);
return render(ui, { wrapper: Wrapper, ...renderOptions });
};
And then in our case we can just import it and use like this:
renderWithState(<User />, { initialState });
Testing components with Redux hooks
The approach presented above is also compatible when using React Redux hooks and selectors, as long as they use the data we provide them in the state.
This is the true advantage of the React Testing Library. No matter what you use for connecting your component to Redux. It only tests what your component renders, without diving deep into implementation details.
I'm regularly publishing my insights on web development.
Consider subscribing to my newsletter.
Visit my blog at slawkolodziej.com to find out more interesting content.
Follow me on Twitter.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.