loading...

Guide: Testing Redux Connected Components with React Testing Library and Jest

slawomirkolodziej profile image Sławek Kołodziej Originally published at slawkolodziej.com Updated on ・3 min read

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);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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();
  });
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
  });
});
Enter fullscreen mode Exit fullscreen mode

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 });
};
Enter fullscreen mode Exit fullscreen mode

And then in our case we can just import it and use like this:

renderWithState(<User />, { initialState });
Enter fullscreen mode Exit fullscreen mode

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.


This article was originally published on slawkolodziej.com. Visit my blog for more content on web development.

Discussion

pic
Editor guide