How to mock useHistory hook in jest?
Asked Answered
H

8

69

I am using UseHistory hook in react router v5.1.2 with typescript? When running unit test, I have got issue.

TypeError: Cannot read property 'history' of undefined.

import { mount } from 'enzyme';
import React from 'react';
import {Action} from 'history';
import * as router from 'react-router';
import { QuestionContainer } from './QuestionsContainer';

describe('My questions container', () => {
    beforeEach(() => {
        const historyHistory= {
            replace: jest.fn(),
            length: 0,
            location: { 
                pathname: '',
                search: '',
                state: '',
                hash: ''
            },
            action: 'REPLACE' as Action,
            push: jest.fn(),
            go: jest.fn(),
            goBack: jest.fn(),
            goForward: jest.fn(),
            block: jest.fn(),
            listen: jest.fn(),
            createHref: jest.fn()
        };//fake object 
        jest.spyOn(router, 'useHistory').mockImplementation(() =>historyHistory);// try to mock hook
    });

    test('should match with snapshot', () => {
        const tree = mount(<QuestionContainer />);

        expect(tree).toMatchSnapshot();
    });
});

Also i have tried use jest.mock('react-router', () =>({ useHistory: jest.fn() })); but it still does not work.

Hymn answered 15/10, 2019 at 10:44 Comment(0)
T
92

I needed the same when shallowing a react functional component that uses useHistory.

Solved with the following mock in my test file:

jest.mock('react-router-dom', () => ({
  useHistory: () => ({
    push: jest.fn(),
  }),
}));
Tartaric answered 27/10, 2019 at 15:58 Comment(8)
For those who use TypeScript, this approach may cause the "React.createElement: type is invalid — expected a string" error if the component uses Link and useHistory at the same time. Erhan's approach won't cause that issue.Piscatorial
Is there a way to capture useHistory().push() invocations?Cockup
But how do you spyOn useHistory function?Claus
Those who are using TypeScript may refer to this: https://mcmap.net/q/281527/-mocking-react-router-dom-for-usehistory-hook-causes-the-following-error-ts2698-spread-types-may-only-be-created-from-object-types/10959940 :)Heigho
@proustibat, can you provide a bit elaborated example ? Also, update the example with .test.js fileWeldon
@Cockup you may want to check the answer I posted for this question ^_^Chevrotain
@Claus you may want to check the answer I posted for this question ^_^Chevrotain
@Chevrotain I solved this by scoping the jest.fn() outside the mock. It can be referenced easily this way. Mocking the router has been abstracted into a helper method already.Cockup
L
56

This one worked for me:

jest.mock('react-router-dom', () => ({
  ...jest.requireActual('react-router-dom'),
  useHistory: () => ({
    push: jest.fn()
  })
}));
Leoine answered 20/11, 2019 at 13:15 Comment(4)
this approach preserves the other react-router-dom functions which you may not want to mockMortonmortuary
@Leoine i have done the same. but again it is throwing error : TypeError: Cannot read property 'history' of undefined. any suggestion ?Sequential
This won't work in TypeScript, as it will give the following error: TS2698: Spread types may only be created from object types.Heigho
For TypeScript support, see this answer.Widner
C
33

Wearing my politician hat I'll dare to state that you're asking the wrong question.

It's not useHistory that you want to mock. Instead you'd just want to feed it with history object which you control.

This also allows you to check for push invocations, just like the 2 top answers (as of writing this).

If that's indeed the case, createMemoryHistory got your back:

import {Router} from 'react-router-dom'
import {createMemoryHistory} from 'history'

test('QuestionContainer should handle navigation', () => {
  const history = createMemoryHistory()
  const pushSpy = jest.spyOn(history, 'push') // or 'replace', 'goBack', etc.
  render(
      <Router history={history}>
        <QuestionContainer/>
      </Router>
  )
  userEvent.click(screen.getByRole('button')) // or whatever action relevant to your UI
  expect(pushSpy).toHaveBeenCalled()
})
Chevrotain answered 3/11, 2020 at 14:52 Comment(3)
I tried this one, my onClick is working ( checked by console logging) but history.push is not being fired , can you help to debug itObellia
@RanjanKumar hard to debug without seeing code... but I'll try anyway: are you spying on 'push' like in my example and yet the toHaveBeenCalled expectation fails?Chevrotain
I think you're right to wear that hat. I didn't really want to mock history. I wanted the error I got when calling useHistory to go away! Rendering inside <Router history={createMemoryHistory()}> did the trick nicely for me!Capitulation
T
29

Here's a more verbose example, taken from working test code (since I had difficulty implementing the code above):

Component.js

  import { useHistory } from 'react-router-dom';
  ...

  const Component = () => {
      ...
      const history = useHistory();
      ...
      return (
          <>
              <a className="selector" onClick={() => history.push('/whatever')}>Click me</a>
              ...
          </>
      )
  });

Component.test.js

  import { Router } from 'react-router-dom';
  import { act } from '@testing-library/react-hooks';
  import { mount } from 'enzyme';
  import Component from './Component';
  it('...', () => {
    const historyMock = { push: jest.fn(), location: {}, listen: jest.fn() };
    ...
    const wrapper = mount(
      <Router history={historyMock}>
        <Component isLoading={false} />
      </Router>,
    ).find('.selector').at(1);

    const { onClick } = wrapper.props();
    act(() => {
      onClick();
    });

    expect(historyMock.push.mock.calls[0][0]).toEqual('/whatever');
  });
Trillbee answered 24/12, 2019 at 6:3 Comment(1)
it worked for me. I had to use this trick with typescript though: <Router history={historyMock as unknown as RouterProps["history"]}>Crossways
H
20

In the Github react-router repo I found that the useHistory hook uses a singleton context, and that you can use a MemoryRouter to provide that context in tests.

import { MemoryRouter } from 'react-router-dom';
const tree =  mount(
    <MemoryRouter>
        // Add the element using history here.
    </MemoryRouter>
);
Hymn answered 15/10, 2019 at 11:28 Comment(1)
Please let us know how we will get ...props value ??Weldon
K
8

A way to mock the push function of useHistory:

import reactRouterDom from 'react-router-dom';
jest.mock('react-router-dom');

const pushMock = jest.fn();
reactRouterDom.useHistory = jest.fn().mockReturnValue({push: pushMock});

Then, how to check if the function have been called:

expect(pushMock).toHaveBeenCalledTimes(1);
expect(pushMock).toHaveBeenCalledWith('something');
Ki answered 27/4, 2021 at 15:24 Comment(0)
L
3

This works for me, I was having problems with useLocation too

jest.mock('react-router-dom', () => ({
  useHistory: () => ({
    push: jest.fn()
  }),
  useLocation: jest.fn().mockReturnValue({
    pathname: '/another-route',
    search: '',
    hash: '',
    state: null,
    key: '5nvxpbdafa'
})}))
Latakia answered 13/12, 2021 at 14:26 Comment(0)
F
2

I found the above answers very helpful. However I missed the ability to spy and actually test functionality. But simply naming the mock function first solved that for me.

const mockPush = jest.fn();
jest.mock('react-router-dom', () => ({
  useHistory: () => {
    const push = () => mockPush ();
    return { push };
  },
}));
Fawcett answered 11/3, 2021 at 11:29 Comment(1)
Why so cumbersome? This should be written like this: useHistory: () => ({ push: mockPush })Chevrotain

© 2022 - 2024 — McMap. All rights reserved.