60

Until now, in unit tests, react router match params were retrieved as props of component. So testing a component considering some specific match, with specific url parameters, was easy : we just had to precise router match's props as we want when rendering the component in test (I'm using enzyme library for this purpose).

I really enjoy new hooks for retrieving routing stuff, but I didn't find examples about how to simulate a react router match in unit testing, with new react router hooks ?

Remi Deprez
  • 603
  • 1
  • 3
  • 5

6 Answers6

64

The way I ended up solving it was by mocking the hooks in my tests using jest.mock:

// TeamPage.test.js
jest.mock('react-router-dom', () => ({
  ...jest.requireActual('react-router-dom'), // use actual for all non-hook parts
  useParams: () => ({
    companyId: 'company-id1',
    teamId: 'team-id1',
  }),
  useRouteMatch: () => ({ url: '/company/company-id1/team/team-id1' }),
}));

I use jest.requireActual to use the real parts of react-router-dom for everything except the hooks I'm interested in mocking.

Mario Petrovic
  • 4,128
  • 5
  • 26
  • 43
Markus-ipse
  • 5,406
  • 2
  • 25
  • 32
58

I looked at the tests for hooks in the react-router repo and it looks like you have to wrap your component inside a MemoryRouter and Route. I ended up doing something like this to make my tests work:

import {Route, MemoryRouter} from 'react-router-dom';

...

const renderWithRouter = ({children}) => (
  render(
    <MemoryRouter initialEntries={['blogs/1']}>
      <Route path='blogs/:blogId'>
        {children}
      </Route>
    </MemoryRouter>
  )
)

Hope that helps!

Mr. Nun.
  • 707
  • 10
  • 26
  • 4
    The problem is mocking the new `react-router-dom` hooks. Wrapping your component in a MemoryRouter is definitely what you want to do though for any component under test that is within a router. There are numerous patterns for creating a reusable wrapper such as https://testing-library.com/docs/example-react-router – Jens Bodal Nov 13 '19 at 09:30
  • 3
    This answer should be accepted, less intrusive, more correct – Mr. Nun. May 03 '20 at 08:46
  • Thank you for this answer and your comment @JensBodal. Of course there are clear examples in the documentation but I always seem to jump to SO first lol! – thul Jan 27 '21 at 21:28
22

In your component use hooks as below

import {useLocation} from 'react-router';

const location = useLocation()

In your test spy on reactRouter Object as below

import routeData from 'react-router';

const mockLocation = {
  pathname: '/welcome',
  hash: '',
  search: '',
  state: ''
}
beforeEach(() => {
  jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation)
});
Mario Petrovic
  • 4,128
  • 5
  • 26
  • 43
Suchin
  • 269
  • 3
  • 5
3

If you're using react-testing-library for testing, you can get this mock to work like so.

jest.mock('react-router-dom', () => ({
    ...jest.requireActual('react-router-dom'),
    useLocation: () => ({ state: { email: 'school@edu.ng' } }),
}));

export const withReduxNRouter = (
    ui,
    { store = createStore(rootReducer, {}) } = {},
    {
    route = '/',
    history = createMemoryHistory({ initialEntries: [ route ] }),
    } = {}
) => {
    return {
    ...render(
        <Provider store={store}>
        <Router history={history}>{ui}</Router>
        </Provider>
    ),
    history,
    store,
    };
};

You should have mocked react-router-dom before it has been used to render your component. I'm exploring ways to make this reusable

autopoietic
  • 155
  • 5
chidimo
  • 1,826
  • 2
  • 20
  • 33
2

I am trying to get if the push function in useHistory is called by doing that but I can't get the mocked function calls...

const mockHistoryPush = jest.fn();

jest.mock('react-router-dom', () => ({
    ...jest.requireActual('react-router-dom'),
    useHistory: () => ({
      push: mockHistoryPush,
    }),
  }));

fireEvent.click(getByRole('button'));
expect(mockHistoryPush).toHaveBeenCalledWith('/help');

It says that mockHistoryPush is not called when the button has onClick={() => history.push('/help')}

Albert Alises
  • 668
  • 1
  • 5
  • 15
  • jest mocks hoist the mocked module before anything else, thus your `mockHistoryPush` won't be seen at runtime. Instead, within your test, do something like `import * as ReactRouterDom from 'react-router-dom'; jest.spyOn(ReactRouterDom, 'useHistory').returnValue({ push: mockHistoryPush, })` – Jens Bodal Nov 13 '19 at 09:32
  • @JensBodal I just tried that and got an "TypeError: Cannot set property useHistory of [object Object] which has only a getter", will update if I find a solution – Jason Rogers Nov 17 '19 at 22:43
  • 1
    Any news on that @JasonRogers ? :'( – Albert Alises Nov 25 '19 at 11:14
  • I'm having the same issue currently. Seems impossible to mock / test this situation. – Alex Jan 30 '20 at 13:21
  • Mocking history.push is explained here: https://stackoverflow.com/questions/58524183/how-to-mock-history-push-with-the-new-react-router-hooks-using-jest – autopoietic May 29 '20 at 10:57
-2

If using the enzyme library, I found a much less verbose way to solve the problem (using this section from the react-router-dom docs):

import React from 'react'
import { shallow } from 'enzyme'
import { MemoryRouter } from 'react-router-dom'
import Navbar from './Navbar'

it('renders Navbar component', () => {
  expect(
    shallow(
      <MemoryRouter>
        <Navbar />
      </MemoryRouter>
    )
  ).toMatchSnapshot()
})
George
  • 11
  • 1
  • 7