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>
);
}
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 yourApp
component; - Always render the
App
component in your tests (never child components likeLocations
); - 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} />
And the specific page for a particular location id:
<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:
If the id
param is truthy, then it will display information for that particular 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>
);
}
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 />);
});
Uh oh
When we run this test with Jest, we get this ugliness:
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 };
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";
Re-run the tests, and voila!
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 });
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
}
);
}
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"] });
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 : ["/"];
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,
});
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>
);
};
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();
});
Here, weâre looking for that Berkeley header that we should see at "/locations/berkeley"
â and finding it!
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;
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>
);
};
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>
);
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)