Prevent any routing misfortune in your React app

3
minutes
Mis à jour le
15/9/2019

Share this post

The story of a routing regression in my React app and how an extremely simple test prevents any future regression.

#
Front-end
#
Testing

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

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

Should we test this?

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:

  • it raises the alarm in case of regressions
  • it saves us time by not having to test all the cases manually every time (less frustrating and time effective)
  • it documents the components rendered by the routes and their edge cases for new developers coming in

I however looked it up, and found no official nor clear way to test this.

How do we test this?

To be a good test, it must fill in some requirements. It should be:

  • Robust: We are testing the behavior and not the implementation. It should not care about what’s inside the rendered component (I don’t want to have to mock all of react-intl, redux states … for each component. And the test results should not be impacted by those)
  • Useful: raise an error when the rendered component is not the one expected
  • Fast

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