29

I am using enzyme+mocha+chai to test my react-redux project. Enzyme provides shallow to test component behavior. But I didn't find a way to test the router. I am using react-router as below:

<Router history={browserHistory}>
     ...
        <Route path="nurse/authorization" component{NurseAuthorization}/>
     ...
  </Route>

I want to test this route nurse/authorization refer to NurseAuthorization component. How to test it in reactjs project?

EDIT1

I am using react-router as the router framework.

JohnAllen
  • 7,317
  • 9
  • 41
  • 65
Joey Yi Zhao
  • 37,514
  • 71
  • 268
  • 523

4 Answers4

26

You can wrap your router inside a component in order to test it.

Routes.jsx

export default props => (
  <Router history={browserHistory}>
    ...
    <Route path="nurse/authorization" component{NurseAuthorization}/>
    ...
  </Route>
)

index.js

import Routes from './Routes.jsx';
...

ReactDOM.render(<Routes />, document.getElementById('root'));

Then you have to shallow render your Routes component, and you are able to create an object map to check the correspondance between path and related component.

Routes.test.js

import { shallow } from 'enzyme';
import { Route } from 'react-router';
import Routes from './Routes.jsx';
import NurseAuthorization from './NurseAuthorization.jsx';

it('renders correct routes', () => {
  const wrapper = shallow(<Routes />);
  const pathMap = wrapper.find(Route).reduce((pathMap, route) => {
    const routeProps = route.props();
    pathMap[routeProps.path] = routeProps.component;
    return pathMap;
  }, {});
  // { 'nurse/authorization' : NurseAuthorization, ... }

  expect(pathMap['nurse/authorization']).toBe(NurseAuthorization);
});

EDIT

In case you want to additionally handle the case of render props:

const pathMap = wrapper.find(Route).reduce((pathMap, route) => {
  const routeProps = route.props();
  if (routeProps.component) {
    pathMap[routeProps.path] = routeProps.component;
  } else if (routeProps.render) {
    pathMap[routeProps.path] = routeProps.render({}).type;
  }
  return pathMap;
}, {});

It will work only in case you render directly the component you want to test (without extra wrapper).

<Route path="nurse/authorization" render{() => <NurseAuthorization />}/>
Freez
  • 7,208
  • 2
  • 19
  • 29
  • I followed your instruction but got below error on the line 'const wrapper = shallow();' - Object is not a constructor (evaluating 'Component(publicProps, publicContext, updateQueue)') – Joey Yi Zhao Jan 14 '17 at 11:52
  • BTW I am using 'react-router' – Joey Yi Zhao Jan 14 '17 at 11:53
  • @ZhaoYi Routes must be a React component (see Routes.jsx) – Freez Jan 16 '17 at 10:33
  • This is great! It worked in App.js: import Routes from "./Routes" export default class App extends Component { render() { return (
    ); } } in index.js: import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; ReactDOM.render( , document.getElementById('root') );
    – compiledweird May 17 '17 at 18:27
  • 2
    Great answer, but be aware since it doesn't handle the case of using `render` instead of `component` directly: ` } />` – Guilherme Agostinelli Sep 02 '19 at 18:06
  • Yes, how do we check for the cases where we use render ? – Hello Nov 15 '19 at 22:50
  • I think you cannot handle it automatically using the way I wrote. The `render` prop receive a function as value and call it injecting router props as argument. – Freez Nov 16 '19 at 03:03
  • Awesome, am looking to do that actually. Could you please give an example for the same? – Hello Nov 16 '19 at 05:09
  • I have the below code: const wrapper = shallow(); const pathMap = wrapper.find(Route).reduce((pathMap, route) => { const routeProps = route.props(); pathMap[routeProps.path] = routeProps.render(routeProps, match); return pathMap; }, {}); jestExpect(pathMap['/some']).toBe(ContainerABC); – Hello Nov 16 '19 at 05:25
  • I edited my answer with a way to handle both `component` and `render` props. – Freez Nov 16 '19 at 05:39
  • Thank you much. It works for me, except I need to pass in the 'match' property to my render() method, along with router props. How do I pass these properties? – Hello Nov 16 '19 at 05:56
  • You can inject a mock of match property `pathMap[routeProps.path] = routeProps.render({ match: { params: {} } }).type;`. – Freez Nov 16 '19 at 06:06
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/202453/discussion-between-hello-and-freez). – Hello Nov 16 '19 at 06:21
  • it's not worth the pain writing these kinds of tests. Don't test your routes to begin with. Just test lower-level behavior. – PositiveGuy Sep 22 '20 at 20:32
  • @PositiveGuy I agree with you that is not the best way to unit test the routes, but I give a solution to the exposed issue. Changing testing strategy is not the solution to the actual problem, it's a way to avoid this problem to happen. You could write another answer with your advice because I think it deserve more visibility. – Freez Sep 23 '20 at 04:16
  • the pathMap is not correct with nested routes, I guess it should iterate them in some sort of recurive manner – raquelhortab May 25 '22 at 09:29
2

I had my paths defined in another file for the dynamic router, so I am also testing that all the routes I am rendering as Routes are defined in my paths.js constants:

it('Routes should only have paths declared in src/routing/paths.js', () => {
  const isDeclaredInPaths = (element, index, array) => {
    return pathsDefined.indexOf(array[index]) >= 0;
  }
  expect(routesDefined.every(isDeclaredInPaths)).to.be.true;
});
Diego Ferri
  • 2,657
  • 2
  • 27
  • 35
Capuchin
  • 3,465
  • 6
  • 28
  • 40
2

This will only pass if the component is rendered successfully: It works with Redux and react-router including hooks.

import React from "react";

import { expect } from "chai";
import { mount } from "enzyme";
import { MemoryRouter, Route } from "react-router-dom";
import { createMockStore } from "redux-test-utils";
import { Provider } from "react-redux";


...
describe("<MyComponent />", () => {
    it("renders the component", () => {
    let props = {
      index: 1,
      value: 1
    };
    let state = {};

    const wrapper = mount(
      <Provider store={createMockStore(state)}>
        <MemoryRouter initialEntries={["/s/parameter1"]}>
          <Route path="/s/:camera">
            <MyComponent {...props} />
          </Route>
        </MemoryRouter>
      </Provider>
    );

    expect(wrapper.find(ProcessedFrames.WrappedComponent)).to.have.lengthOf(1);
  });
});
TacoEater
  • 2,115
  • 20
  • 22
0

Tested for react-router-dom v6

Based on @Freez 's answer, I implemented a recursive function that returns a correct url map even if you are using nested routes.

You just need to add this once in setupTests.js for jest tests to be able to use it in any test:

function recursiveGetPathMap(route, parentPath){
  let pathMap = {};
  const routeProps = route.props();
  let path = parentPath + (parentPath.length == 0 || parentPath[parentPath.length-1] == '/' ? '' : '/') + routeProps.path;
  pathMap[path] = routeProps.element.type;
  route.children(Route).forEach(el=>{
    pathMap = {...pathMap, ...recursiveGetPathMap(el, path)};
  });
  return pathMap;
}

global.getPathMap = (wrapper)=>{

  let pathMap = {};
  wrapper.find(Routes).children(Route).forEach(el =>{
    pathMap = {...pathMap, ...recursiveGetPathMap(el, "")};
  });

  return pathMap;
}

Example:

App.js

...
<Routes>
    <Route path="/" element={<Layout/>}>
        <Route path="users" element={<Users/>}>
            <Route path=":name" element={<Profile/>}/>
        </Route>
    </Route>
</Routes>
...

App.test.js

...
it('whatever', ()=>{
    const component = <App/>;
    const wrapper = shallow(component);
    const pathMap = getPathMap(wrapper);
    expect(pathMap['/']).toBe(Layout);
    expect(pathMap['/users']).toBe(Users);
    expect(pathMap['/users/:name']).toBe(Profile);
});
...

The output of console.log(pathMap) in that example is:

{
    '/': [Function: Layout],
    '/users': [Function: Users],
    '/users/:name': [Function: Profile]
}

Note that if you have a route without path (index route):

<Route index element={<SomeComponent/>}/>

the route will be like /somepath/somepath/undefined

raquelhortab
  • 430
  • 4
  • 13