How to test components using new react router hooks?

React Router

React Router Problem Overview


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 ?

React Router Solutions


Solution 1 - React Router

Edit: The proper way of doing this the way described in Catalina Astengo's answer as it uses the real router functionality with just the history/routing state mocked rather than mocking the entire hook.

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.

Solution 2 - React Router

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!

Solution 3 - React Router

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)
});

Solution 4 - React Router

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: '[email protected]' } }),
}));

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

Solution 5 - React Router

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')}

Solution 6 - React Router

My use case was unit testing a custom hook using using useLocation(). I had to override the inner properties of useLocation which was read-only.


\\ foo.ts

export const useFoo = () => {

   const {pathname} = useLocation();


\\ other logic

return ({
          \\ returns whatever thing here
       });
}

/*----------------------------------*/

\\ foo.test.ts

\\ other imports here

import * as ReactRouter from 'react-router';


Object.defineProperty(ReactRouter, 'useLocation', {
   value: jest.fn(),
   configurable: true,
   writable: true,
});

describe("useFoo", () => {


       it(' should do stgh that involves calling useLocation', () => {

           const mockLocation = {
               pathname: '/path',
               state: {},
               key: '',
               search: '',
               hash: ''
           };


         const useLocationSpy =  jest.spyOn(ReactRouter, 'useLocation').mockReturnValue(mockLocation)



          const {result} = renderHook(() => useFoo());
         
           expect(useLocationSpy).toHaveBeenCalled();


       });
 });

Solution 7 - React Router

A slight variation of the above solutions which includes several params and query strings for a more complex scenario. This is easy to abstract into a utility function similar to a few above which can be reused by other tests.

short version

      <MemoryRouter
        initialEntries={[
          '/operations/integrations/trello?business=freelance&businessId=1&pageId=1&pageName=Trello',
        ]}
      >
        <Route path="/operations/:operation/:location">
          <OperationPage />
        </Route>
      </MemoryRouter>

Longer version:

The example snippets below include a full example of the test file, component and logs to help leave little room for interpretation.

includes:

  • react 16
  • redux 7
  • react-router-dom 5
  • typescript
  • thunk
  • sagas
  • @testing-library/react 11

operations.spec.tsx

import React from 'react'
import { MemoryRouter, Route } from 'react-router-dom'
import { render, screen } from '@testing-library/react'
import { Provider } from 'react-redux'
import { createStore, applyMiddleware, compose } from 'redux'
import createDebounce from 'redux-debounced'
import thunk from 'redux-thunk'
import createSagaMiddleware from 'redux-saga'
import rootReducer from 'redux/reducers/rootReducer'
import OperationPage from '../operation'
import { initialState } from '../mock'
import '@testing-library/jest-dom' // can be moved to a single setup file

const sagaMiddleware = createSagaMiddleware()
const middlewares = [thunk, sagaMiddleware, createDebounce()]
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose

const store = createStore(
  rootReducer,
  // any type only until all reducers are given a type
  initialState as any,
  composeEnhancers(applyMiddleware(...middlewares))
)

const Wrapper: React.FC = ({ children }) => <Provider store={store}>{children}</Provider>

describe('Operation Page - Route', () => {
  it('should load', async () => {

    const Element = () => (
      <MemoryRouter
        initialEntries={[
          '/operations/integrations/trello?business=freelance&businessId=1&pageId=1&pageName=Trello',
        ]}
      >
        <Route path="/operations/:operation/:location">
          <OperationPage />
        </Route>
      </MemoryRouter>
    )
    render(<Element />, { wrapper: Wrapper })
    // logs out the DOM for further testing
    screen.debug()
  })
})

logs and the component via operations.tsx. Got lazy including the all types (via typescript) for this component but outside of scope :)

import React from 'react'
import { useParams, useLocation } from 'react-router-dom'
import { connect } from 'react-redux'
import queryString from 'query-string'

const OperationPage = (): JSX.Element => {
  const { search } = useLocation()
  const queryStringsObject = queryString.parse(search)
  const { operation, location } = useParams<{ operation: string; location: string }>()

  console.log(
    '>>>>>queryStringsObject',
    queryStringsObject,
    '\n search:',
    search,
    '\n operation:',
    operation,
    '\n location:',
    location
  )
  return <div>component</div>
}

const mapStateToProps = (state) => {
  return {
    test: state.test,
  }
}

export default connect(mapStateToProps, {})(OperationPage)

terminal where the tests are running

>>>>>queryStringsObject [Object: null prototype] {
  business: 'freelance',
  businessId: '1',
  pageId: '1',
  pageName: 'Trello'
}
 search: ?business=freelance&businessId=1&pageId=1&pageName=Trello
 operation: integrations
 location: trello


 PASS  src/__tests__/operations.spec.tsx
  Operation Page - Route
    āœ“ should load (48 ms)

Test Suites: 1 passed, 1 total
Tests:       0 skipped, 1 passed, 1 total
Snapshots:   0 total
Time:        2.365 s
Ran all test suites related to changed files.

Solution 8 - React Router

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()
})

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRemi DeprezView Question on Stackoverflow
Solution 1 - React RouterMarkus-ipseView Answer on Stackoverflow
Solution 2 - React RouterCatalina AstengoView Answer on Stackoverflow
Solution 3 - React RouterSuchinView Answer on Stackoverflow
Solution 4 - React RouterchidimoView Answer on Stackoverflow
Solution 5 - React RouterAlbert AlisesView Answer on Stackoverflow
Solution 6 - React RouterexaucaeView Answer on Stackoverflow
Solution 7 - React Routerariel guzmanView Answer on Stackoverflow
Solution 8 - React RouterGeorgeView Answer on Stackoverflow