Testing React Apps that use React Router
Tags: testing, react, reactrouter
Published: 2022-06-02 22:52:22

_Note: This post is about React Router 5, not React Router 6_

---
So your React app, like a lot of modern apps, uses React Router to get users from one page to another. And you, like any thorough tester, want to account for React Router in your testing. This post will help you learn about the syntax to get you started testing React Router with your app.

## Our App

***Note****: you can find the* [*code for this project on GitHub*](https://github.com/bonnie/react-router-testing)*. It will not win any awards for design.* 😝

For the purposes of this post, we’ll imagine a restaurant website that looks like this:

```jsx
import { Switch, Route, Link } from "react-router-dom";
import Home from "./Home";
import Menus from "./Menus";
import Locations from "./Locations";
import About from "./About";export default function App() {
  return (
    <div>
      <nav>
        <Link to="/">Our Restaurant</Link>
        <Link to="/menus">Menus</Link>
        <Link to="/locations">Locations</Link>
        <Link to="/about">About</Link>
      </nav>
      {/* routing */}
      <Switch>
        <Route path="/menus" component={Menus} />
        <Route path="/locations/:id" component={Locations} />             
        <Route path="/locations" component={Locations} />
        <Route path="/about" component={About} />
        <Route path="/" component={Home} />
      </Switch>
    </div>
  );
}
```

**Note that the Router is not included in the App component.** I’ve included it in *src/index.js* instead. By omitting it from the App component, we are able to use a test router in our tests which is more easily manipulated.

### What if using a test router feels too artificial?
If you have qualms about using a different Router in your tests vs. production, you’ll probably want to to:

*   Include the `Router` in your `App` component;
*   Always render the `App` component in your tests (never child components like `Locations`);
*   Navigate to your pages in tests by finding and clicking links on the page

The positives of this approach: you don’t need to read the rest of this post 🙃 (and your test setup will be less complicated). The negatives: you can’t immediately load a routing history (the current page and previous pages) in test setup; you need to go through all the user interactions to build the history.

## The Locations Component

If you’re still here, then you’re interested in learning about using a different router in your tests. In this post, we’ll focus on the general locations page with no URL parameter:

```jsx
    <Route path="/locations" component={Locations} />
```

And the specific page for a particular location id:

```jsx
    <Route path="/locations/:id" component={Locations} />
```

The Locations component uses `useParams` to get the `:id` URL param. If the `id` param is falsy, that means the route was `/locations` with no param, and, the component displays a list of location links:

![list of restaurant locations (San Francisco, Berkeley, Oakland) displayed at the `/locations` route](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ufms46f0b7998kbcfcvg.png)

If the `id` param is truthy, then it will display information for that particular location:

![the `/locations/berkeley` route displays information about the berkeley location](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/56kq8eatw12sxd7ottr6.png)

### Example code for the Locations component

```jsx
import { useParams, Link } from "react-router-dom";

export default function Locations() {
// We can use the `useParams` hook here to access
// the dynamic pieces of the URL.
const { id } = useParams();// in a real app, this info would come from the server
  
const locationData = {
  "san-francisco": {
    name: "San Francisco",
    address: "123 Main Street",
  },
  berkeley: {
    name: "Berkeley",
    address: "456 First Street",
  },
  oakland: {
    name: "Oakland",
    address: "789 Elm Street",
  },
};// no id? then just list links for all the locations
  if (!id) {
    return (
      <div>
       <h1>Locations</h1>
         <ul>
         {Object.entries(locationData).map(([id, data]) => {
            return (
            <li key={id}>
              <Link to={`/locations/${id}`}>{data.name}</Link>
            </li>
          );
        })}
        </ul>
      </div>
    );
  }// if there's an id URL parameter, give information about this location
  const data = locationData[id];
  return (
    <div>
      <h1>Location: {data.name}</h1>
      <p>{data.address}</p>
    </div>
  );
}
```

## Including Router Context when Testing

**Note**: We will use* [*Jest*](http://jestjs.io/) *as a test runner and* [*Testing Library*](https://testing-library.com/) *for rendering React components in this blog post.*

Let’s make a simple test for our Locations component in *Locations.test.js*. No big deal, just seeing that it renders without error:

```jsx
import { render } from "@testing-library/react";
import Locations from "./Locations";

test("renders without error", () => {
  render(<Locations />);
});
```

### Uh oh

When we run this test with Jest, we get this ugliness:

![Jest output with error at useParams: “Cannot read property ‘match’ of undefined”](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2kvc5f9awzi2v5u3cqyj.png)

The problem is, we’re trying to use `useParams` outside of a Router provider. No wonder Jest is confused.

### The Solution

As luck would have it, Testing Library makes it easy to adapt its `render` function to wrap with whatever your UI elements might need — be it the React Router provider, or any other type of provider (see “Including the Router and other Providers” below for rendering with multiple providers).

The Testing Library React docs describe how to create [a custom render that includes a wrapper](https://testing-library.com/docs/react-testing-library/setup#custom-render). In our case, we could create this *test-utils.jsx* file in our *src* directory:

```js
import { render } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";

// create a customRender that wraps the UI in a memory Router
const customRender = (ui, options) => {
  return render(ui, { wrapper: MemoryRouter, ...options });
}

// re-export everything
export * from "@testing-library/react";

// override render method
export { customRender as render };
```

Now, if we import `render` (and `screen`, and any other Testing Library imports) from this file instead of from `@testing-library/react` all of our rendered components will be wrapped in a `MemoryRouter` before being rendered.

***Note****: The React Router docs recommend* `*MemoryRouter*` *for testing; you can see other options in the* [*React Router testing docs*](https://v5.reactrouter.com/web/guides/testing)*).*

For the new and improved *Locations.test.js*, simply change the first line to import from the *test-utils* module:

```jsx
    import { render } from "./test-utils";
```

Re-run the tests, and voila!

![passing tests!](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5pf3rexzme38pixprts2.png)

<figcaption>aaah, much&nbsp;better</figcaption>

### Testing Location URL params

Ok, so the component renders without error. That’s one test, but I want to know that it renders the *right thing* without error. How can I specify what the route is?

We’ll need to update our `render` in *test-utils.jsx* to accept an initial route, which we can feed to the `MemoryRouter`. The “Memory” part means it stores routes in memory, as opposed to using a browser.

**Note**: In this case, we only need one initial route; in other cases, you might want to pass a whole array of historical routes (for example, if you were testing authentication that returned the user to the referring route).*

### Using a function for the render wrapper

When we didn’t care about specifying initial routes, it was fine to specify the render `wrapper` as the `MemoryRouter` function in *test-utils.jsx*:

```js
    render(ui, { wrapper: MemoryRouter, ...options });
```

However, now we want to add a prop to `MemoryRouter`, and things are going to get more interesting. The prop we want to add is `initialEntries` , as recommended in the [“Starting at Specific Routes” docs for React Router Testing](https://reactrouter.com/web/guides/testing/starting-at-specific-routes).

Because of that prop, we are going to have to make a new function for the `wrapper` value. The `wrapper` value has always been a function (`MemoryRouter` is simply a functional component after all), but now we need to dig in and get our hands a little dirty.

The function for `wrapper` takes, among other props, `children`. The [Testing Library React setup docs for Custom Render](https://testing-library.com/docs/react-testing-library/setup#custom-render) show how to use the `children` prop in a `wrapper` function arg. This code does the same thing our previous code:

```jsx
const MemoryRouterWithInitialRoutes = ({ children }) =>
  <MemoryRouter>{children}</MemoryRouter>;

const customRender = (ui, options) => {
  return render(
    ui,
    {
      wrapper: MemoryRouterWithInitialRoutes,
      ...options
    }
  );
}
```

But now we have some more room to maneuver.

### Passing initial entries to the wrapper function

We want to be able to pass the initial entries to the options for the `render` function, something like:

```jsx
    render(<App />, { initialRoutes: ["/locations/berkeley"] });
```

Then we need to get this to the `MemoryRouterWithInitialRoutes` function we wrote above, as the `initialEntries` prop.

### Step 1. Define `initialRoutes` in `customRender`

It’s important to have a default `initialRoutes` of `["/"]`, since `MemoryRouter` spews errors if the array is empty. We can take care of that default in `customRender` (no matter what the options array may or may not contain) like so:

```jsx
const initialRoutes =
  options && options.initialRoutes ? options.initialRoutes : ["/"];
```

### Step 2. Pass `initialRoutes` to `MemoryRouterWithInitialRoutes` function

Then we can pass our newly-defined `initialRoutes` to `MemoryRouterWithInitialRoutes` (along with the default args, so the function can still access `children`):

```jsx
return render(ui, {
  wrapper: (args) =>
    MemoryRouterWithInitialRoutes({
      ...args,
      initialRoutes,
    }),
    ...options,
});
```

### Step 3. Use `initialRoutes` parameter in `MemoryRouterWithInitialRoutes` function

and finally, `MemoryRouterWithInitialRoutes` can make use of `initialRoutes`:

```jsx
const MemoryRouterWithInitialRoutes = ({ children, initialRoutes }) => {
  return (
    <MemoryRouter initialEntries={initialRoutes}>
      {children}
    </MemoryRouter>
  );
};
```

#### Initial Routes in Action

Whew, that was a lot of setup. The good news is, it’s relatively simple to use a in a test file. Let’s use it to test that the route does the right thing when we navigate to `"/locations/berkeley"` :

```jsx
test("renders berkeley page", () => {
  render(<App />, { initialRoutes: ["/locations/berkeley"] });
  const berkeleyHeader = screen.getByRole(
    "heading",
    { name: /berkeley/i }
  );
  expect(berkeleyHeader).toBeInTheDocument();
});
```

Here, we’re looking for that **Berkeley** header that we should see at `"/locations/berkeley"` — and finding it!

![the `/locations/berkeley` route displays information about the berkeley location](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/56kq8eatw12sxd7ottr6.png)

### Why App and not Locations?

You might be wondering: why do the above examples render the `App` component and not the `Locations` component? It turns out, when you remove components from the React Router `Switch` component, you don’t have access to the `[match](https://reactrouter.com/web/api/match)` object (which contains the URL params, along with other route information).

You can fix this by using `useRouteMatch` in *Locations.jsx* instead of `useParams`:

```jsx
// useParams: will not work with rendering Locations component
// const { id } = useParams();

// useRouteMatch: will work with rendering Locations component
const match = useRouteMatch("/locations/:id");
const id = match && match.params && match.params.id
  ? match.params.id
  : null;
```

I would **not recommend** this, however, as it’s generally not a great practice to complicate your production code merely for the sake of tests.

## Including the Router and other Providers

Remember `MemoryRouterWithInitialRoutes` ?

```jsx
const MemoryRouterWithInitialRoutes = ({ children, initialRoutes }) => {
  return (
    <MemoryRouter initialEntries={initialRoutes}>
      {children}
    </MemoryRouter>
  );
};
```

This can be updated to add as many providers as you’d like. For example, if you want to add a [Redux](https://react-redux.js.org/api/provider) provider and a [React Query](https://react-query.tanstack.com/) provider:

```jsx
import { Provider } from 'react-redux';
import { QueryClient, QueryClientProvider } from 'react-query';

const MemoryRouterWithInitialRoutes = ({ children, initialRoutes }) => (
const queryClient = new QueryClient();
  <MemoryRouter initialEntries={initialRoutes}>
    <QueryClientProvider client={queryClient}>
      <Provider store={store}>
        {children}
      </Provider>
    </QueryClientProvider>
  </MemoryRouter>
);
```

**Note**: You will have to create the* `*store*` *for Redux* [*the same way you would for the actual Provider*](https://redux.js.org/api/createstore/) *(not included in the code above). The good news is, you can also use this temporary test* `*store*` *to set up initial state for your tests.*

You might want to update the name of the function at this point from `MemoryRouterWithInitialRoutes` to `Providers`. 🙃

## Conclusion

I hope this is enough to get you started with testing apps that use React Router. As you can see, the setup is not simple! Fortunately, once you have the `MemoryRouter` wrapping your `render`, it’s more straightforward to apply routes in the test functions.