A few weeks ago, my team suffered a regression: one developer decided to re-order all the routes alphabetically and one page was not rendered anymore.
We are currently using react-router-dom v4.3.1
and before the regression, our code basically looked like:
// routes.js
import {
AllPosts,
SpecificPost,
} from './pages';
...
<Switch>
...
<Route path="/posts/post/:postId" component={SpecificPost} />
<Route path="/posts" component={AllPosts} />
...
</Switch>
...
On the React Router documentation, you can read: Switch
renders the first child <Route>
or <Redirect>
that matches the location.
The second path="/posts"
is here more generic and goes first alphabetically. Therefore, after re-ordering the routes, the component rendered on a route like posts/post/8
was AllPosts
, instead of SpecificPost
. Hence our regression.
We could have used exact
on <Route exact path="/posts" component={AllPosts} />
but that would mean that routes like posts/blablabla
would not be matched: the AllPosts
component would not be our fallback on those routes anymore.
The code looked purely declarative to us, but the fact is that it contains logic.
For instance, there is logic in the order of declaration of the <Route>
’s: the app does not render the same one way or the other. This component should therefore be tested.
Indeed, testing it brings multiple benefits:
I however looked it up, and found no official nor clear way to test this.
To be a good test, it must fill in some requirements. It should be:
Therefore, we didn’t want to render the real pages in the test (they require a lot of data and are quite long to render).
So we jest.mock
’ed all of our pages imports, replacing them with simple <div id="myPageName" />
that we’ll use to test.
Here is our solution for the testing of our <Switch>
component:
// routes.test.js
import React from "react";
import { mount } from "enzyme";
import { MemoryRouter, Route } from "react-router-dom";
import Routes from "routes";
// Mocking the components imports in routes.js
jest.mock("./pages", () => {
const pages = ["AllPosts", "SpecificPost"];
return pages.reduce(
(accumulator, pageName) => ({
...accumulator,
[pageName]: () => <div id={pageName} />
}),
{}
);
});
// Create the wrapper from the routeName for each test
const getWrapper = (routeName: string) =>
mount(
<MemoryRouter initialEntries={[routeName]}>
<Route component={Routes} />
</MemoryRouter>
);
describe("Routes", () => {
it('routes "/posts" to AllPosts', () => {
const wrapper = getWrapper("/posts");
expect(wrapper.find("#AllPosts")).toHaveLength(1);
expect(wrapper.find("#SpecificPost")).toHaveLength(0);
});
it('routes "/posts/blablabla" to AllPosts (fallback)', () => {
const wrapper = getWrapper("/posts/blablabla");
expect(wrapper.find("#AllPosts")).toHaveLength(1);
expect(wrapper.find("#SpecificPost")).toHaveLength(0);
});
it('routes "/posts/post/:postId" to SpecificPost', () => {
const wrapper = getWrapper("/posts/post/8");
expect(wrapper.find("#SpecificPost")).toHaveLength(1);
expect(wrapper.find("#AllPosts")).toHaveLength(0);
});
});
That’s it! Tell me what you think about it ;)
PS: If you don’t want to test this component and make it entirely declarative, you can for instance have a default fallback component, like a 404 page, and set all the other routes as exact
.
// routes.js
...
<Switch>
<Route exact path="/posts" component={AllPosts} />
<Route exact path="/posts/post/:postId" component={SpecificPost} />
<Route component={NotFoundPage} />
</Switch>
...