How to mock history.push with the new React Router Hooks using Jest
Asked Answered
A

5

55

I am trying to mock history.push inside the new useHistory hook on react-router and using @testing-library/react. I just mocked the module like the first answer here: How to test components using new react router hooks?

So I am doing:

//NotFound.js
import * as React from 'react';
import { useHistory } from 'react-router-dom';


const RouteNotFound = () => {
  const history = useHistory();
  return (
    <div>
      <button onClick={() => history.push('/help')} />
    </div>
  );
};

export default RouteNotFound;
//NotFound.test.js
describe('RouteNotFound', () => {
  it('Redirects to correct URL on click', () => {
    const mockHistoryPush = jest.fn();

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

    const { getByRole } = render(
        <MemoryRouter>
          <RouteNotFound />
        </MemoryRouter>
    );

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

But mockHistoryPush is not called... What am I doing wrong?

Actiniform answered 23/10, 2019 at 13:38 Comment(0)
L
58

You actually do not need to mock react-router-dom (at least for v5) as it provides a bunch of testing tools: https://v5.reactrouter.com/web/guides/testing

To check your history is actually changed, you can use createMemoryHistory and inspect its content:

import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Menu } from './Menu';
import { createMemoryHistory } from 'history'
import { Router } from 'react-router-dom';

test('triggers path change', () => {
  const history = createMemoryHistory();

  render(
    <Router history={history}>
      <Menu />
    </Router>
  );

  const aboutItem = screen.getByText('About');
  expect(aboutItem).toBeInTheDocument();

  userEvent.click(aboutItem);
  expect(history.length).toBe(2);
  expect(history.location.pathname).toBe('/about');
});
Longlegged answered 17/5, 2021 at 16:15 Comment(6)
This should be the accepted answer. Mocking react-router can cause unnecessary complexity.Aurochs
The link is changed. Please update if possible.Bandstand
any idea how in RR6?Langsyne
@Langsyne I assume you should use MemoryRouter: reactrouter.com/docs/en/v6/routers/memory-router You have a test sample on this documentation page.Longlegged
@Hubert, but this approach won't be an option if you use MemoryRouter. MemoryRouter allows you to set the initial path (initialEntries) while Router doesn't. I totally agree that mocking react-router-dom is often complex thoughAntipater
I am trying out this solution but I always get element type is invalid: Expected a string (for built-in components) or a class / function (for composite components) but got: undefined. There are nothing wrong with my imports so I'm confused and wondering anyone else faced this issue as well? My react-router-dom version is 5.2.0Cassiopeia
D
109

Use jest.mock in module scope will automatically be hoisted to the top of the code block. So that you can get the mocked version react-router-dom in NotFound.jsx file and your test file.

Besides, we only want to mock useHistory hook, so we should use jest.requireActual() to get the original module and keep other methods as the original version.

Here is the solution:

NotFound.jsx:

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

const RouteNotFound = () => {
  const history = useHistory();
  return (
    <div>
      <button onClick={() => history.push('/help')} />
    </div>
  );
};

export default RouteNotFound;

NotFound.test.jsx:

import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { render, fireEvent } from '@testing-library/react';
import RouteNotFound from './NotFound';

const mockHistoryPush = jest.fn();

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

describe('RouteNotFound', () => {
  it('Redirects to correct URL on click', () => {
    const { getByRole } = render(
      <MemoryRouter>
        <RouteNotFound />
      </MemoryRouter>,
    );

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

Unit test result with 100% coverage:

PASS  src/stackoverflow/58524183/NotFound.test.jsx
  RouteNotFound
    ✓ Redirects to correct URL on click (66ms)

--------------|----------|----------|----------|----------|-------------------|
File          |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
--------------|----------|----------|----------|----------|-------------------|
All files     |      100 |      100 |      100 |      100 |                   |
 NotFound.jsx |      100 |      100 |      100 |      100 |                   |
--------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        5.133s, estimated 11s

Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/58524183

Delegate answered 23/12, 2019 at 8:7 Comment(4)
@slidershowp2 can you please help me in #61928763Deserving
Hey @slideshowp2, thanks for this answer. I was trying to do this but I had the jest.mock inside of the describe block and it wasn't working. Do you know why this is the case? I found this thread but I still don't understand it. github.com/facebook/jest/issues/2582Manila
You saved me. Thank you very muchIssus
I'm having the same issue expect my component using RouteComponentPropsalong with the useHIstory() hook. The hook returns undefined with the components inside the test. Is there a solution for mocking useHistory() with a component that uses RouteComponentProps?Trigon
L
58

You actually do not need to mock react-router-dom (at least for v5) as it provides a bunch of testing tools: https://v5.reactrouter.com/web/guides/testing

To check your history is actually changed, you can use createMemoryHistory and inspect its content:

import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Menu } from './Menu';
import { createMemoryHistory } from 'history'
import { Router } from 'react-router-dom';

test('triggers path change', () => {
  const history = createMemoryHistory();

  render(
    <Router history={history}>
      <Menu />
    </Router>
  );

  const aboutItem = screen.getByText('About');
  expect(aboutItem).toBeInTheDocument();

  userEvent.click(aboutItem);
  expect(history.length).toBe(2);
  expect(history.location.pathname).toBe('/about');
});
Longlegged answered 17/5, 2021 at 16:15 Comment(6)
This should be the accepted answer. Mocking react-router can cause unnecessary complexity.Aurochs
The link is changed. Please update if possible.Bandstand
any idea how in RR6?Langsyne
@Langsyne I assume you should use MemoryRouter: reactrouter.com/docs/en/v6/routers/memory-router You have a test sample on this documentation page.Longlegged
@Hubert, but this approach won't be an option if you use MemoryRouter. MemoryRouter allows you to set the initial path (initialEntries) while Router doesn't. I totally agree that mocking react-router-dom is often complex thoughAntipater
I am trying out this solution but I always get element type is invalid: Expected a string (for built-in components) or a class / function (for composite components) but got: undefined. There are nothing wrong with my imports so I'm confused and wondering anyone else faced this issue as well? My react-router-dom version is 5.2.0Cassiopeia
S
0

Let suppose you have created File method in Order.JSx to execute a click event. If you check i am using
const history = useHistory();

This is react functional component 
import { useHistory } from 'react-router-dom';
        function ReactOrders({ prams }) {
        const history = useHistory();
    
        const viewOrder = (order) => {
            **history.push({**
                pathname: `/order/${order.orderId}/${order.orderCompany}/${order.orderType}`,
            });
        };
    
        const renderOrder = (order, index) => {
            return (           
                    <div className="view-order">                   
  <button className="button button-secondary" onClick={() => viewOrder(order)}>
                                View Order
                            </button>
                        </div>              
               
            );
        };

Now in OrdersTest.js

import { createMemoryHistory } from 'history';

let history = createMemoryHistory();

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

 describe("and it returns multiple orders", () => {    
             
                beforeEach(() => {                     
                    _component = shallow(                     
                            <Orders {...props} orders={_orderData.salesOrderHeader} fetching={false} />
                        , {
                        context: { router: history }, // Provide the mock history object as context
                    });                  
                });
       
                fit("shows a link to the order details", () => {                  
                    _component.find(".details .view-order .button")
                        .forEach((b, i) => {
                            b.simulate("click");
                        });                
                    expect(mockHistoryPush).toHaveBeenCalledWith({ "pathname": "/order/orderBert/company1/CO" });
                    expect(mockHistoryPush).toHaveBeenCalledWith({ "pathname": "/order/jobbert/company2/SO" });
                });
      
            });
Sweepstakes answered 7/8, 2023 at 15:34 Comment(0)
B
0

For anyone out there with react-router-dom V4, this worked for me:

    // HomeButton.test.jsx
    import { render, screen, act } from '@testing-library/react';
    import userEvent from '@testing-library/user-event';
    
    import { HomeButton } from './HomeButton';
    
    describe('HomeButton', () => {
      // the test might throw an error about required properties, it's depend on your component's dependencies.
      const mockHistory = {
        push: jest.fn(),
      }
    
      it('should go to home after click', () => {
        await act(async () => {
          render(
            <Router history={mockHistory}>
              <HomeButton />
            </Router>
          )
    
          userEvent.click(screen.getByTestId('button'))
        })
    
        expect(mockHistory.push).toBeCalledTimes(1)
        expect(mockHistory.push).toBeCalledWith("/home")
      })
    })
Bleacher answered 14/9, 2023 at 10:58 Comment(0)
A
-3

This may be helpful. You can use jest.fn() to mock history.push().

const historyMock = { push: jest.fn() }   

expect(historyMock.push.mock.calls[0]).toEqual([
        {
          pathname: "/profile", // URL
          search: , // search-data
        },
      ]);
Atavism answered 29/11, 2021 at 5:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.