Guide: Testing Redux Connected Components with React Testing Library and Jest
Tags: react, redux, testing, javascript
Published: 2020-10-21 18:38:30

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.

```jsx
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.

```jsx
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

```jsx
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.

```jsx
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:

```jsx
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.

```jsx
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:

```jsx
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](https://slawkolodziej.com/newsletter).
Visit my blog at [slawkolodziej.com](https://slawkolodziej.com) to find out more interesting content.
Follow me on [Twitter](https://twitter.com/slawkolodziej).