DEV Community

Bonnie Schulkin
Bonnie Schulkin

Posted on

Testing React Apps that use React Router

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. It will not win any awards for design. 😝

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



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>
  );
}


Enter fullscreen mode Exit fullscreen mode

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:



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


Enter fullscreen mode Exit fullscreen mode

And the specific page for a particular location id:



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


Enter fullscreen mode Exit fullscreen mode

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  raw `/locations` endraw  route

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

the  raw `/locations/berkeley` endraw  route displays information about the berkeley location

Example code for the Locations component



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>
  );
}


Enter fullscreen mode Exit fullscreen mode

Including Router Context when Testing

Note: We will use* Jest as a test runner and Testing Library 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:



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

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


Enter fullscreen mode Exit fullscreen mode

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”

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. In our case, we could create this test-utils.jsx file in our src directory:



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 };


Enter fullscreen mode Exit fullscreen mode

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).

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



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


Enter fullscreen mode Exit fullscreen mode

Re-run the tests, and voila!

passing tests!

aaah, much better

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:



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


Enter fullscreen mode Exit fullscreen mode

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.

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 show how to use the children prop in a wrapper function arg. This code does the same thing our previous code:



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

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


Enter fullscreen mode Exit fullscreen mode

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:



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


Enter fullscreen mode Exit fullscreen mode

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:



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


Enter fullscreen mode Exit fullscreen mode

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):



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


Enter fullscreen mode Exit fullscreen mode

Step 3. Use initialRoutes parameter in MemoryRouterWithInitialRoutes function

and finally, MemoryRouterWithInitialRoutes can make use of initialRoutes:



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


Enter fullscreen mode Exit fullscreen mode

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" :



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


Enter fullscreen mode Exit fullscreen mode

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

the  raw `/locations/berkeley` endraw  route displays information about the berkeley location

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:



// 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;


Enter fullscreen mode Exit fullscreen mode

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 ?



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


Enter fullscreen mode Exit fullscreen mode

This can be updated to add as many providers as you’d like. For example, if you want to add a Redux provider and a React Query provider:



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>
);


Enter fullscreen mode Exit fullscreen mode

Note: You will have to create the* *store* for Redux the same way you would for the actual Provider (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.

Top comments (0)