< Articles


React Testing Library - Advanced Cases

While I covered most of the typical React Testing Library use cases for components here, I thought I’d add a couple of advanced cases in a separate post.

The first case I want to mention is wrapping components in test providers. Common examples of when this might be necessary is when your component is dependent on Redux or React Router.

First, let’s set up our example component. Let’s pretend our Redux State for this component contains a userMetadata key which store the user’s name:

import React from "react";
import { useSelector } from "react-redux";

export const UserMetadata = () => {
    const userMetadata = useSelector((state) => {
        return state.userMetadata;
    });

    return <p>{userMetadata.name}</p>;
};

Pretty simple component right? If we go the traditional route of testing though, we’ll definitely run into problems. Why? Because React is expecting our component to be wrapped in the Redux Provider. Here’s how we would handle that:

import { UserMetadata } from "./index";
import { render } from "@testing-library/react";
import { Provider as ReduxProvider } from "react-redux";
import { configureStore } from "../../../wherever";

describe("UserMetadata", () => {
    it("should show the user's name", () => {
        // configureStore isn't shown here but is typically
        // a way to create a base redux state
        const store = configureStore({
            userMetadata: {
                name: "Tom Haverford",
            },
        });
        const renderResult = render(
            <ReduxProvider store={store}>
                <UserMetadata />
            </ReduxProvider>
        );

        renderResult.getByText("Tom Haverford");
    });
});

By wrapping our component in Redux, we ensure the component has access to everything it needs.

The second case I want to mention is when we have complex custom hooks that we’re using as part of our component. Let’s envision our hook wasn’t just a selector but that it actually made internal API requests if the data wasn’t available. I’m not going to write all that out but here’s some psuedo code to visualize the behavior:

import React from "react";

export const useUserMetadata = () => {
    // 1. Check if the user already exists in redux.
    // 2. If the user isn't available in Redux, make an API Request.
    // 3. Return either an empty string (in the interim) or the
    // user metadata
};

Now let’s rewrite our component to use this new hook:

import React from "react";
import { useUserMetadata } from "../../../wherever";

export const UserMetadata = () => {
    const userMetadata = useUserMetadata();

    return <p>{userMetadata.name}</p>;
};

Using the real useUserMetadata hook would force us to wrap the component in the ReduxProvider as well as mock out the initial API request. That’s way too much work, especially considering we’re not trying to unit test the hook. That should have already been done in isolation.

Instead, our goal is to test if this component works correctly, assuming our expectations are met with the hook. Here’s how we might accomplish that:

import { UserMetadata } from "./index";
import { render } from "@testing-library/react";
import * as useUserMetadataFile from "../../../useUserMetadata";

describe("UserMetadata", () => {
    it("should show the user's name", () => {
        jest.spyOn(useUserMetadataFile, "useUserMetadata").mockImplementation(
            () => {
                return {
                    name: "Tom Haverford",
                };
            }
        );
        const renderResult = render(<UserMetadata />);

        renderResult.getByText("Tom Haverford");
    });
});

Notice that we’re importing the entire file as its own module and then spying on the hook we need to mock out. This can be a simple way to test components that rely on complex hooks.

That covers some of the more complex use cases. In the next post, we’ll go over testing custom hooks with the React Testing Library.