circuit

Testing React Router with Jest

How to write unit tests that check if you're routing requests correctly using React Router with Jest




Image of a stock chart being viewed on a mobile device

Writing a unit test that checks if React Router is correctly configured to route requests in your app seems simple enough. And admittedly, it doesn't take much code to do it. But that doesn't mean it's easy to figure out. My online search didn't produce any out-of-the-box code examples and I spent several hours trying to get a decent working solution for this problem.

If you're facing the same challenge, here's what to look out for. Note that the solution is written for a React app that uses functional components. So you might have to tweak it a bit if you're working with class components.

The first section will explain the final solution I arrived at. Later sections will focus on potential problems and provide a more in-depth explanation of why I ended up doing things the way I did.

The Idea of the Solution

The app that this article is looking to test contains the following App.js file:

import { HomePage } from "./components/home/HomePage";
import { ArticlePage } from "./components/article/ArticlePage";
import { AuthorPage } from "./components/author/AuthorPage";
import { PageHeader } from "./components/common/page-header/PageHeader";
import PageNotFound from "./components/PageNotFound";
import { Container } from "react-bootstrap";

function App() {
  return (
    <Container>
      <PageHeader />
      <Switch>
        <Route exact path="/" component={HomePage} />
        <Route exact path="/article/:articleId" component={ArticlePage} />
        <Route exact path="/author/:authorId" component={AuthorPage} />
        <Route component={PageNotFound} />
      </Switch>
    </Container>
  );
}

export default App;

The PageHeader component of the app is rendered for every route. HomePageArticlePage and AuthorPage each have a designated route that matches that page. If the route doesn't match either of them, a PageNotFound component is displayed. This is the desired behavior that should be covered by tests.

As mentioned in the introduction, all the listed components were implemented as functional components in React.

Unit tests for routing requests to desired components shouldn't test or depend on the content of the components. That's a separate behavior that should be tested in the unit tests of each individual component.

The idea behind this solution is to avoid dependencies on the components by mocking the components. To see if the correct component was rendered, I set up mocks to return a hardcoded string instead of their normal value. Each component returns a different hardcoded string. The test then asserts that upon rendering the App for a certain route, a hardcoded string that belongs to the appropriate component can be found in the result of the render.

Since PageHeader is always rendered regardless of the route, I decided to check its presence in each test. However, creating a separate test that checks only if PageHeader is rendered and omitting it from other tests would also be a valid solution in this case.

Below is a step by step guide on how to implement this.

Implementation

Firstly, we need to mock the modules of all the components the Router routes to. The modules that contain the components need to be imported and then mocked using the jest.mock function.

import { HomePage } from "./components/home/HomePage";
import { ArticlePage } from "./components/article/ArticlePage";
import { AuthorPage } from "./components/author/AuthorPage";
import { PageHeader } from "./components/common/page-header/page-header";
import PageNotFound from "./components/PageNotFound";

jest.mock("./components/home/HomePage");
jest.mock("./components/common/page-header/page-header");
jest.mock("./components/article/ArticlePage");
jest.mock("./components/author/AuthorPage");
jest.mock("./components/PageNotFound");

After that, we can start writing tests for concrete examples. The first test will check if HomePage component and PageHeader are rendered when there's no route specified (meaning the route is "/").

The first step in arranging this test is to mock the return values of HomePage and PageHeader to be hardcoded string values that we can later search for. Be careful not to use the same value for both of them as that would make the test invalid.

test("Should render page header and HomePage on default route", () => {
    // Arrange
    PageHeader.mockImplementation(() => <div>PageHeaderMock</div>);
    HomePage.mockImplementation(() => <div>HomePageMock</div>);
}

Note that the mockImplementation call was made inside the test and that the hardcoded string was wrapped in a div. Both are needed for the solution to work properly and I explain why in the later sections.

Once the mocks are arranged, we can call the render function. In order to pass the route to the App component, I used a MemoryRouterMemoryRouter has a parameter called initialEntries that takes an array of initial routes as inputs. If no route is given, App should render the component that's used for an empty path "/".

In this case we don't need to pass anything to the MemoryRouter since we want to test the behaviour for the default route.

test("Should render page header and HomePage on default route", () => {
    // Arrange
    PageHeader.mockImplementation(() => <div>PageHeaderMock</div>);
    HomePage.mockImplementation(() => <div>HomePageMock</div>);

    // Act
    render(
      <MemoryRouter>
        <App/>
      </MemoryRouter>
    );
}

Finally, we check that the right component was rendered by asserting that there is an element in the rendered document that contains the string we set before.

test("Should render page header and HomePage on default route", () => {
    // Arrange
    PageHeader.mockImplementation(() => <div>PageHeaderMock</div>);
    HomePage.mockImplementation(() => <div>HomePageMock</div>);

    // Act
    render(
      <MemoryRouter>
        <App/>
      </MemoryRouter>
    );

    // Assert
    expect(screen.getByText("PageHeaderMock")).toBeInTheDocument();
    expect(screen.getByText("HomePageMock")).toBeInTheDocument();
}

Below is the complete code testing all the components in App.js. For other components, the initialEntries parameter of the MemoryRouter was populated with an appropriate route. For the PageNotFound component, any route that doesn't belong to another component can be used.

import React from "react";
import App from "./App";
import { HomePage } from "./components/home/HomePage";
import { ArticlePage } from "./components/article/ArticlePage";
import { AuthorPage } from "./components/author/AuthorPage";
import { PageHeader } from "./components/common/page-header/PageHeader";
import { PageNotFound } from "./components/PageNotFound";
import { render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router";
import "@testing-library/jest-dom/extend-expect";

jest.mock("./components/home/HomePage");
jest.mock("./components/common/page-header/PageHeader");
jest.mock("./components/article/ArticlePage");
jest.mock("./components/author/AuthorPage");
jest.mock("./components/PageNotFound");

describe("Tests for App Router", () => {
  test("Should render page header and HomePage on default route", () => {
    // Arrange
    PageHeader.mockImplementation(() => <div>PageHeaderMock</div>);
    HomePage.mockImplementation(() => <div>HomePageMock</div>);

    // Act
    render(
      <MemoryRouter>
        <App />
      </MemoryRouter>
    );

    // Assert
    expect(screen.getByText("PageHeaderMock")).toBeInTheDocument();
    expect(screen.getByText("HomePageMock")).toBeInTheDocument();
  });

  test("Should render page header and ArticlePage for article route", () => {
    // Arrange
    PageHeader.mockImplementation(() => <div>PageHeaderMock</div>);
    ArticlePage.mockImplementation(() => <div>ArticlePageMock</div>);

    // Act
    render(
      <MemoryRouter initialEntries={["/article/1"]}>
        <App />
      </MemoryRouter>
    );

    // Assert
    expect(screen.getByText("PageHeaderMock")).toBeInTheDocument();
    expect(screen.getByText("ArticlePageMock")).toBeInTheDocument();
  });

  test("Should render page header and AuthorPage for author route", () => {
    // Arrange
    PageHeader.mockImplementation(() => <div>PageHeaderMock</div>);
    AuthorPage.mockImplementation(() => <div>AuthorPageMock</div>);

    // Act
    render(
      <MemoryRouter initialEntries={["/author/1"]}>
        <App />
      </MemoryRouter>
    );

    // Assert
    expect(screen.getByText("PageHeaderMock")).toBeInTheDocument();
    expect(screen.getByText("AuthorPageMock")).toBeInTheDocument();
  });

  test("Should render page header and PageNotFound for invalid route", () => {
    // Arrange
    PageHeader.mockImplementation(() => <div>PageHeaderMock</div>);
    PageNotFound.mockImplementation(() => <div>PageNotFoundMock</div>);

    // Act
    render(
      <MemoryRouter initialEntries={["/invalid/route"]}>
        <App />
      </MemoryRouter>
    );

    // Assert
    expect(screen.getByText("PageHeaderMock")).toBeInTheDocument();
    expect(screen.getByText("PageNotFoundMock")).toBeInTheDocument();
  });
});

If you'd like to see the context in which this solution was created, you can view the git repository of the app here.

The minor downfall of this solution is that, in theory, App.js (or another component) could contain an element with the same hardcoded string that's set as a return value of the mock. The test would therefore be ineffective as it could find the element with the given text regardless if the component mock has been rendered or not. So you shouldn't choose an existing text from your app as the hardcoded string in the test.

Why should the text be wrapped in a div?

It doesn't necessarily have to be a div, but a text that the mock returns should be wrapped in a separate HTML element. And the element shouldn't contain anything but the text.

The screen.getByText function looks for an HTML element that contains the exact match of the given text and doesn't contain anything else. (This behaviour can be modified by passing different options. See documentation for more details.)

I originally tried to match the exact text, but didn't wrap the return value in an HTML element like shown below.

test("Should render page header and HomePage on default route", () => {
    // Arrange
    PageHeader.mockImplementation(() => PageHeaderMock);
    HomePage.mockImplementation(() => HomePageMock);

    // Act
    render(
      <MemoryRouter>
        <App/>
      </MemoryRouter>
    );

    // Assert
    expect(screen.getByText("PageHeaderMock")).toBeInTheDocument();
    expect(screen.getByText("HomePageMock")).toBeInTheDocument();
}

The issue with this approach is that PageHeader and HomePage are rendered in the same container in App.js. That means the result of the redner will end up looking like this:

<Container>PageHeaderMock HomePageMock</Container>

The container element now contains the text of both PageHeader mock and HomePage mock. So screen.getByText won't consider this container as a match.

Why is the mockImplementation function called within each test?

If you look at the final code of the test featured above, you'll notice that the mockImplementation function is called within each test to mock the return value. This introduces quite a lot of redundancy (and room for error) for the PageHeader mock. It's set to return the same value in each test.

Wouldn't it be better to create a global definition for the mock? Or define it only once within the describe section?

I tried to do both, but with no success. It resulted with an error message stating: Invalid variable access: _jsxFileName.

At the time of writing, there's an open issue on GitHub that was raised for this problem.

Checking if a component was rendered

My original idea was to directly test if rendering App would render the appropriate component for a selected route. This is a slightly more elegant solution because it avoids adding hardcoded strings as return values to the mock components and then searching for them.

However, I couldn't find a way to mock a functional component in React and then check if the mock was rendered in the test.

The solutions I found online for testing this functionality focused on using Enzyme. Most of them suggested using Enzyme's shallow function for rendering the parent component (in this case App). However, shallow doesn't work for React Router --- deep render is necessary to test this.

I found an article on how to test the router using Jest and Enzyme that uses the mount function from Enzyme. This function should enable you to perform a deep render and check if the right component was rendered.

If you're using Enzyme with Jest, this article could be a useful read.

The comment section of the article also contains some interesting potential improvements. However, be mindful that the article was written in 2017.

Closing Remarks

I hope you found this article helpful. If you have a proposal for improving this solution, feel free to reach out to me. I might update the article with potential improvements or issues that people might face.

Thank you for reading.




Continue Learning