Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to mock useRouter? #7479

Closed
nikitowsky opened this issue May 31, 2019 · 38 comments
Closed

How to mock useRouter? #7479

nikitowsky opened this issue May 31, 2019 · 38 comments

Comments

@nikitowsky
Copy link
Contributor

nikitowsky commented May 31, 2019

Question about Next.js

I'm want to make sure my component renders correctly with useRouter hook (actually I'm trying to understand how new dynamic routing works), so I have code:

import React from 'react';
import { NextPage } from 'next';
import { useRouter } from 'next/router';

const UserInfo : NextPage = () => {
  const router = useRouter();
  const { query } = router;

  return <div>Hello {query.user}</div>;
};

export default UserInfo;

And what I'm trying is:

// test
import { render, cleanup, waitForElement } from '@testing-library/react';

import UserInfo from './$user';

// somehow mock useRouter for $user component!!!

afterEach(cleanup);

it('Should render correctly on route: /users/nikita', async () => {
  const { getByText } = render(<UserInfo />);

  await waitForElement(() => getByText(/Hello nikita!/i));
});

But I get an error TypeError: Cannot read property 'query' of null which points on const router = useRouter(); line.

P. S. I know dynamic routing is available on canary verions just for now and might change, but I get a problem with router, not with WIP feature (am I?).

@ijjk
Copy link
Member

ijjk commented May 31, 2019

Hi, this feature is still experimental but useRouter uses React.useContext to consume the context from next-server/dist/lib/router-context. To mock it you would need to wrap it in the context provider from there similar to this line

@ijjk ijjk closed this as completed May 31, 2019
@nikitowsky
Copy link
Contributor Author

nikitowsky commented Jun 1, 2019

@ijjk Hi, thank you!
I don't know if I'm doing it right, but test passes 😂

import { render, cleanup, waitForElement } from '@testing-library/react';
import { createRouter } from 'next/router';
import { RouterContext } from 'next-server/dist/lib/router-context';

const router = createRouter('', { user: 'nikita' }, '', {
  initialProps: {},
  pageLoader: jest.fn(),
  App: jest.fn(),
  Component: jest.fn(),
});

import UserInfo from './$user';

afterEach(cleanup);

it('Should render correctly on route: /users/nikita', async () => {
  const { getByText } = render(
    <RouterContext.Provider value={router}>
      <UserInfo />
    </RouterContext.Provider>,
  );

  await waitForElement(() => getByText(/Hello nikita!/i));
});

If there is more abstract way to mock query params, so I'd be able to pass actual route (/users/nikita for example) and pass path to file? What do you think?

@ijjk

This comment has been minimized.

@nikitowsky
Copy link
Contributor Author

@ijjk that make sense. Thank you a lot!

@renellc
Copy link

renellc commented Jul 10, 2019

Is there any way to mock useRouter using Enzyme+Jest? I've been searching online for a bit and the only relevant results that come up is this issue.

@francisprovost
Copy link

I managed to mock it this way.

import * as nextRouter from 'next/router';

nextRouter.useRouter = jest.fn();
nextRouter.useRouter.mockImplementation(() => ({ route: '/' }));

@steviewonders3
Copy link

jest.spyOn works for me too -

import React from 'react'
import { render } from '@testing-library/react'
import ResultsProductPage from 'pages/results/[product]'

const useRouter = jest.spyOn(require('next/router'), 'useRouter')

describe('ResultsProductPage', () => {
  it('renders - display mode list', () => {
    useRouter.mockImplementationOnce(() => ({
      query: { product: 'coffee' },
    }))
    const { container } = render(
      <ResultsProductPage items={[{ name: 'mocha' }]} />
    )
    expect(container).toMatchSnapshot()
  })
})

@aeksco
Copy link

aeksco commented Sep 20, 2019

I ended up mocking it like this, I only need the useRouter export so this worked well enough for my purposes:

jest.mock("next/router", () => ({
    useRouter() {
        return {
            route: "/",
            pathname: "",
            query: "",
            asPath: "",
        };
    },
}));

@t-lock
Copy link

t-lock commented Oct 23, 2019

If anyone is here looking to mock useRouter simply to avoid interference from an imperative prefetch, then this dead simple mock will work

jest.mock("next/router", () => ({
  useRouter() {
    return {
      prefetch: () => null
    };
  }
}));

an example use case would be a form component that includes something like:

  const router = useRouter();
  useEffect(() => {
    router.prefetch("/success");
    if (confirmSuccess) {
      doStuff();
      router.push( {pathname: "/success" } )
    }
  }, [data]);

@chrisfosterelli
Copy link

@ijjk Has that behaviour changed in the latest version? I had to import from next/dist/next-server/lib/router-context. It wouldn't recognize the context if I installed next-server separately.

@ghost
Copy link

ghost commented Dec 23, 2019

I have the same exact problem.
We're under next 9. None of the solutions using the RouterContext.Provider actually work.
The only way my test pass is using @aeksco solution as a global object above the test. Otherwise useRouter is always undefined.
This is not ideal as I cannot set different parameters for my test.
Any ideas on this ?

EDIT
I made it work with a global mock of the next/router import and a spyOn on the mock, which allows me to call mockImplementation(() => ({// whatever you want}) in each test.
It looks something like :

jest.mock("next/router", () => ({
  useRouter() {
    return {
      route: "",
      pathname: "",
      query: "",
      asPath: "",
    };
  },
}));

const useRouter = jest.spyOn(require("next/router"), "useRouter");

Then in the tests :

useRouter.mockImplementation(() => ({
      route: "/yourRoute",
      pathname: "/yourRoute",
      query: "",
      asPath: "",
    }));

This is not ideal but at least it works for me

@chrisfosterelli
Copy link

FWIW this is what I've settled on:

import { RouterContext } from 'next/dist/next-server/lib/router-context'
import { action } from '@storybook/addon-actions'
import PropTypes from 'prop-types'
import { useState } from 'react'
import Router from 'next/router'

function RouterMock({ children }) {
  const [pathname, setPathname] = useState('/')

  const mockRouter = {
    pathname,
    prefetch: () => {},
    push: async newPathname => {
      action('Clicked link')(newPathname)
      setPathname(newPathname)
    }
  }

  Router.router = mockRouter

  return (
    <RouterContext.Provider value={mockRouter}>
      {children}
    </RouterContext.Provider>
  )
}

RouterMock.propTypes = {
  children: PropTypes.node.isRequired
}

export default RouterMock

I needed something that worked both in Storybook and in Jest. This seems to do the trick, you just set <Routermock> somewhere up the component tree. It's not ideal because I don't love overriding Router.router constantly.

I think an official mocking solution would be lovely :)

@mbrowne
Copy link

mbrowne commented Feb 7, 2020

@smasontst's method worked for us, but be careful with mockImplementationOnce()...if your component needs to render more than once during your test, you'll find that it's not using your mock router on the second render and your test will fail. It's probably best to always use mockImplementation() instead, unless you have a specific reason to use mockImplementationOnce().

@aeksco
Copy link

aeksco commented Feb 17, 2020

I had to revise my initial implementation since I needed unique useRouter state on a test-by-test basis. Took a page from the example provided by @nterol24s and updated it to act as a utility function I can call within my tests:

// Mocks useRouter
const useRouter = jest.spyOn(require("next/router"), "useRouter");

/**
 * mockNextUseRouter
 * Mocks the useRouter React hook from Next.js on a test-case by test-case basis
 */
export function mockNextUseRouter(props: {
    route: string;
    pathname: string;
    query: string;
    asPath: string;
}) {
    useRouter.mockImplementationOnce(() => ({
        route: props.route,
        pathname: props.pathname,
        query: props.query,
        asPath: props.asPath,
    }));
}

I can now do things like:

import { mockNextUseRouter } from "@src/test_util";

describe("Pricing Page", () => {
    
    // Mocks Next.js route
    mockNextUseRouter({
        route: "/pricing",
        pathname: "/pricing",
        query: "",
        asPath: `/pricing?error=${encodeURIComponent("Uh oh - something went wrong")}`,
    });

    test("render with error param", () => {
        const tree: ReactTestRendererJSON = Renderer.create(
            <ComponentThatDependsOnUseRouter />
        ).toJSON();
        expect(tree).toMatchSnapshot();
    });
});

Note the comment by @mbrowne - you'll hit the same issue with this approach, but you can split the example above into mockNextUseRouter and mockNextUseRouterOnce functions if you need.

Also a BIG 👍 for an official mocking solution @timneutkens

@mattcarlotta
Copy link
Contributor

mattcarlotta commented Apr 12, 2020

For anyone who wants a globally mocked Router instance, you can place a __mocks__ folder anywhere and target the next/router package like so:

__mocks__/next/router/index.js (has to follow this folder structure pattern!)

This example below targets Router.push and Router.replace:

jest.mock("next/router", () => ({
  // spread out all "Router" exports
  ...require.requireActual("next/router"),

  // shallow merge the "default" exports with...
  default: {
    // all actual "default" exports...
    ...require.requireActual("next/router").default,

    // and overwrite push and replace to be jest functions
    push: jest.fn(),
    replace: jest.fn(),
   },
}));

// export the mocked instance above
module.exports = require.requireMock("next/router");

Now, anywhere there's an import Router from "next/router"; it will be the mocked instance. You'll also be able to add mockImplementation functions on them since they'll be globally mocked.
If you want this instance to be reset for each test, then in your jest.json add a clearMocks property.

For reference, here's the Router structure if you want to target a specific export:

{
  __esModule: true,
  useRouter: [Function: useRouter],
  makePublicRouterInstance: [Function: makePublicRouterInstance],
  default: { 
    router: null,
    readyCallbacks: [ 
      [Function],
      [Function],
      [Function],
      [Function],
      [Function],
      [Function] 
    ],
    ready: [Function: ready],
    push: [Function],
    replace: [Function],
    reload: [Function],
    back: [Function],
    prefetch: [Function],
    beforePopState: [Function] },
    withRouter: [Function: withRouter],
    createRouter: [Function: createRouter],
    Router: { 
      [Function: Router]
      events: { 
        on: [Function: on],
        off: [Function: off],
        emit: [Function: emit] 
       } 
    },
    NextRouter: undefined 
  }
}

In addition, if you have to mount components that happen to utilize withRouter or useRouter and you don't want to mock them but still want to create some tests against/around them, then you can utilize this HOC wrapper factory function for testing:

import { createElement } from "react";
import { mount } from "enzyme";
import { RouterContext } from "next/dist/next-server/lib/router-context";
// Important note: The RouterContext import will vary based upon the next version you're using;
// in some versions, it's a part of the next package, in others, it's a separate package

/**
 * Factory function to create a mounted RouterContext wrapper for a React component
 *
 * @function withRouterContext
 * @param {node} Component - Component to be mounted
 * @param {object} initialProps - Component initial props for setup.
 * @param {object} state - Component initial state for setup.
 * @param {object} router - Initial route options for RouterContext.
 * @param {object} options - Optional options for enzyme's mount function.
 * @function createElement - Creates a wrapper around passed in component (now we can use wrapper.setProps on root)
 * @returns {wrapper} - a mounted React component with Router context.
*/
export const withRouterContext = (
  Component,
  initialProps = {},
  state = null,
  router = {
    pathname: "/",
    route: "/",
    query: {},
    asPath: "/",
  },
  options = {},
) => {
  const wrapper = mount(
    createElement(
      props => ( 
        <RouterContext.Provider value={router}>
          <Component { ...props } /> 
        </RouterContext.Provider>
      ),
      initialProps,
    ),
    options,
  );
  if (state) wrapper.find(Component).setState(state);
  return wrapper;
};

Example usage:

import React from "react";
import withRouterContext from "./path/to/reusable/test/utils"; // alternatively you can make this global
import ExampleComponent from "./index";

const initialProps = {
  id: "0123456789",
  firstName: "John",
  lastName: "Smith"
};

const router = {
  pathname: "/users/$user",
  route: "/users/$user",
  query: { user: "john" },
  asPath: "/users/john",
};

const wrapper = withRouterContext(ExampleComponent, initialProps, null, router);

...etc

Why use this? Because it allows you to have a reusable mounted React component wrapped in a Router context; and most importantly, it allows you to call wrapper.setProps(..) on the root component!

@kenberkeley
Copy link

kenberkeley commented May 10, 2020

import { useRouter } from 'next/router'

jest.mock('next/router', () => ({
  __esModule: true,
  useRouter: jest.fn()
}))

describe('XXX', () => {
  it('XXX', () => {
    const mockRouter = {
      push: jest.fn() // the component uses `router.push` only
    }
    ;(useRouter as jest.Mock).mockReturnValue(mockRouter)
    // ...
    expect(mockRouter.push).toHaveBeenCalledWith('/hello/world')
  })
})

@coler-j
Copy link

coler-j commented Jul 15, 2020

None of these solutions worked for me. The "correct" workflow is also described here in the Jest docs: https://jestjs.io/docs/en/es6-class-mocks#spying-on-methods-of-our-class

However, I can see the mock, but it does not record calls...

@flybayer
Copy link
Contributor

Here's my current test-utils.tsx. I like this a lot better than using a global mock.

import React from 'react';
import { render as defaultRender } from '@testing-library/react';
import { RouterContext } from 'next/dist/next-server/lib/router-context';
import { NextRouter } from 'next/router';

export * from '@testing-library/react';

// --------------------------------------------------
// Override the default test render with our own
//
// You can override the router mock like this:
//
// const { baseElement } = render(<MyComponent />, {
//   router: { pathname: '/my-custom-pathname' },
// });
// --------------------------------------------------
type DefaultParams = Parameters<typeof defaultRender>;
type RenderUI = DefaultParams[0];
type RenderOptions = DefaultParams[1] & { router?: Partial<NextRouter> };

export function render(
  ui: RenderUI,
  { wrapper, router, ...options }: RenderOptions = {},
) {
  if (!wrapper) {
    wrapper = ({ children }) => (
      <RouterContext.Provider value={{ ...mockRouter, ...router }}>
        {children}
      </RouterContext.Provider>
    );
  }

  return defaultRender(ui, { wrapper, ...options });
}

const mockRouter: NextRouter = {
  basePath: '',
  pathname: '/',
  route: '/',
  asPath: '/',
  query: {},
  push: jest.fn(),
  replace: jest.fn(),
  reload: jest.fn(),
  back: jest.fn(),
  prefetch: jest.fn(),
  beforePopState: jest.fn(),
  events: {
    on: jest.fn(),
    off: jest.fn(),
    emit: jest.fn(),
  },
  isFallback: false,
};

@ujwal-setlur
Copy link

@flybayer thanks! Works great!

@narze
Copy link

narze commented Aug 19, 2020

@flybayer's solution works for me, however I have to specify the return type on render function

import { render as defaultRender, RenderResult } from '@testing-library/react'

...

export function render(
  ui: RenderUI,
  { wrapper, router, ...options }: RenderOptions = {}
): RenderResult { ... }

@ron766
Copy link

ron766 commented Aug 31, 2020

For anyone who wants a globally mocked Router instance, you can place a __mocks__ folder anywhere and target the next/router package like so:

__mocks__/next/router/index.js (has to follow this folder structure pattern!)

This example below targets Router.push and Router.replace:

jest.mock("next/router", () => ({
  // spread out all "Router" exports
  ...require.requireActual("next/router"),

  // shallow merge the "default" exports with...
  default: {
    // all actual "default" exports...
    ...require.requireActual("next/router").default,

    // and overwrite push and replace to be jest functions
    push: jest.fn(),
    replace: jest.fn(),
   },
}));

// export the mocked instance above
module.exports = require.requireMock("next/router");

Now, anywhere there's an import Router from "next/router"; it will be the mocked instance. You'll also be able to add mockImplementation functions on them since they'll be globally mocked.
If you want this instance to be reset for each test, then in your jest.json add a clearMocks property.

For reference, here's the Router structure if you want to target a specific export:

{
  __esModule: true,
  useRouter: [Function: useRouter],
  makePublicRouterInstance: [Function: makePublicRouterInstance],
  default: { 
    router: null,
    readyCallbacks: [ 
      [Function],
      [Function],
      [Function],
      [Function],
      [Function],
      [Function] 
    ],
    ready: [Function: ready],
    push: [Function],
    replace: [Function],
    reload: [Function],
    back: [Function],
    prefetch: [Function],
    beforePopState: [Function] },
    withRouter: [Function: withRouter],
    createRouter: [Function: createRouter],
    Router: { 
      [Function: Router]
      events: { 
        on: [Function: on],
        off: [Function: off],
        emit: [Function: emit] 
       } 
    },
    NextRouter: undefined 
  }
}

In addition, if you have to mount components that happen to utilize withRouter or useRouter and you don't want to mock them but still want to create some tests against/around them, then you can utilize this HOC wrapper factory function for testing:

import { createElement } from "react";
import { mount } from "enzyme";
import { RouterContext } from "next/dist/next-server/lib/router-context";
// Important note: The RouterContext import will vary based upon the next version you're using;
// in some versions, it's a part of the next package, in others, it's a separate package

/**
 * Factory function to create a mounted RouterContext wrapper for a React component
 *
 * @function withRouterContext
 * @param {node} Component - Component to be mounted
 * @param {object} initialProps - Component initial props for setup.
 * @param {object} state - Component initial state for setup.
 * @param {object} router - Initial route options for RouterContext.
 * @param {object} options - Optional options for enzyme's mount function.
 * @function createElement - Creates a wrapper around passed in component (now we can use wrapper.setProps on root)
 * @returns {wrapper} - a mounted React component with Router context.
*/
export const withRouterContext = (
  Component,
  initialProps = {},
  state = null,
  router = {
    pathname: "/",
    route: "/",
    query: {},
    asPath: "/",
  },
  options = {},
) => {
  const wrapper = mount(
    createElement(
      props => ( 
        <RouterContext.Provider value={router}>
          <Component { ...props } /> 
        </RouterContext.Provider>
      ),
      initialProps,
    ),
    options,
  );
  if (state) wrapper.find(Component).setState(state);
  return wrapper;
};

Example usage:

import React from "react";
import withRouterContext from "./path/to/reusable/test/utils"; // alternatively you can make this global
import ExampleComponent from "./index";

const initialProps = {
  id: "0123456789",
  firstName: "John",
  lastName: "Smith"
};

const router = {
  pathname: "/users/$user",
  route: "/users/$user",
  query: { user: "john" },
  asPath: "/users/john",
};

const wrapper = withRouterContext(ExampleComponent, initialProps, null, router);

...etc

Why use this? Because it allows you to have a reusable mounted React component wrapped in a Router context; and most importantly, it allows you to call wrapper.setProps(..) on the root component!

hi, I'm getting this error:

TypeError: require.requireMock is not a function

USED THIS SOLUTION:

jest.mock("next/router", () => ({
  // spread out all "Router" exports
  ...jest.requireActual("next/router"),
  
  // shallow merge the "default" exports with...
  default: {
    // all actual "default" exports...
    ...jest.requireActual("next/router").default,
  
    // and overwrite push and replace to be jest functions
    push: jest.fn(),
    replace: jest.fn(),
  },
}));
  
// export the mocked instance above
module.exports = jest.requireMock("next/router");

@Meemaw
Copy link

Meemaw commented Dec 9, 2020

If anyone is still struggling with this I highly recommend you give next-page-tester a try. It aims to solve these issues and make testing of NextJS applications a breeze.

Note: not everything is supported yet, but it is actively maintained and any issues should be resolved rather quickly.

@ririlli
Copy link

ririlli commented Dec 13, 2020

In case anyone is still trying to find a simple solution to mock the module and test that the router functions are actually being called here's mine:

import { useRouter } from "next/router";

jest.mock("next/router", () => ({
  useRouter: jest.fn(),
}));

describe("Your test", () => {
  it('should...', () => {
    const push = jest.fn();
    useRouter.mockImplementation(() => ({push}));

    // render component

    expect(push).toHaveBeenCalledWith("/test/path");
  })
})

@phegman
Copy link

phegman commented Dec 14, 2020

This was my approach:

import * as nextRouter from "next/router";

type MockUseRouterParams = Partial<nextRouter.NextRouter>;

export const mockUseRouter = ({
  route = "",
  pathname = "",
  query = {},
  asPath = "",
  basePath = "",
  locale = "",
  locales = [],
  defaultLocale = "",
}: MockUseRouterParams) => {
  const actions = {
    push: jest.fn(() => Promise.resolve(true)),
    replace: jest.fn(() => Promise.resolve(true)),
    reload: jest.fn(() => Promise.resolve(true)),
    prefetch: jest.fn(() => Promise.resolve()),
    back: jest.fn(() => Promise.resolve(true)),
    beforePopState: jest.fn(() => Promise.resolve(true)),
    events: {
      on: jest.fn(),
    },
  };

  (nextRouter.useRouter as jest.Mock) = jest.fn(() => ({
    route,
    pathname,
    query,
    asPath,
    basePath,
    locale,
    locales,
    defaultLocale,
    ...actions,
  }));

  return actions;
};

@mbrowne
Copy link

mbrowne commented Dec 14, 2020

@phegman Thanks for sharing. Kind of off-topic, but the TypeScript type in your code can be simplified as follows:

type MockUseRouterParams = Partial<nextRouter.NextRouter>

@phegman
Copy link

phegman commented Dec 14, 2020

@mbrowne that's 🔥! Thanks for the refactor, updated!

@breytex
Copy link

breytex commented Dec 30, 2020

When using the approach of @flybayer , I encountered a weird error message:

Error: Uncaught [TypeError: Cannot read property 'catch' of undefined]

Seems like, next tries to .catch and .then some of those functions declared in const mockRouter.
Solved it with:

const mockRouter: NextRouter = {
  basePath: '/',
  pathname: '/',
  route: '/',
  query: {},
  asPath: '/',
  push: jest.fn(() => Promise.resolve(true)),
  replace: jest.fn(() => Promise.resolve(true)),
  reload: jest.fn(() => Promise.resolve(true)),
  prefetch: jest.fn(() => Promise.resolve()),
  back: jest.fn(() => Promise.resolve(true)),
  beforePopState: jest.fn(() => Promise.resolve(true)),
  isFallback: false,
  events: {
    on: jest.fn(),
    off: jest.fn(),
    emit: jest.fn(),
  },
};

@JosielHernandezDev
Copy link

im work with on click event on link use next/router hook "useRoute"

const Login = (props) => { const router = useRouter(); return( .... <LinkButton onClick={() => goPage(RECOVER_PASSWORD)} className={'col-12'}>Recover password</LinkButton> ... )

and fix whit

1.- global mock router next js in "mocks\next\router\index.js" file

export default () => jest.mock('next/router', () => ({ __esModule: true, useRouter: jest.fn(), push: jest.fn(), events: { on: jest.fn(), off: jest.fn() }, beforePopState: jest.fn(() => null) }));
and in test file

`
it('should redirect to register page when to have click in register link ', async (done) => {
let registerLink;

const useRouter = jest.spyOn(require('next/router'), 'useRouter')
const push = jest.fn()
useRouter.mockImplementationOnce(() => ({ push }))

wrapper = mount(<LoginPage />);
registerLink = wrapper.find('a').last();
registerLink.simulate('click');

expect(registerLink).toBeTruthy();
expect(push).toHaveBeenCalledWith(REGISTER);

done();

});

`

@Unicornelia
Copy link

I managed to mock it this way.

import * as nextRouter from 'next/router';

nextRouter.useRouter = jest.fn();
nextRouter.useRouter.mockImplementation(() => ({ route: '/' }));

Thanks this worked perfectly!!! 🍰

@lucksp
Copy link

lucksp commented Feb 12, 2021

I managed to mock it this way.

import * as nextRouter from 'next/router';

nextRouter.useRouter = jest.fn();
nextRouter.useRouter.mockImplementation(() => ({ route: '/' }));

ESLint rule may complain about directly reassigning "read-only" imports due to

The members of 'nextRouter' are read-only. eslint no-import-assign

Its possibly better to use jest's mock or custom render as suggested above.

@fkapsahili
Copy link

fkapsahili commented Feb 13, 2021

In case the examples listed here don't exactly work for someone, here is a very simple example that works for me even with router.push calls in a component:

At the top of your test:

import { useRouter } from "next/router";

jest.mock("next/router", () => ({
  useRouter: jest.fn(),
}));

and then inside the describe block:

    const push = jest.fn();
    useRouter.mockImplementation(() => ({
      push,
      pathname: "/",
      route: "/",
      asPath: "/",
      query: "",
    }));

    fireEvent.click(screen.getByText("some text...."));
    expect(push).toHaveBeenCalledWith("/your-expected-route");

@suvasishm
Copy link

suvasishm commented Feb 15, 2021

My component using useRouter looks like the folowing:

import { FunctionComponent, useEffect } from 'react';
import { useRouter } from 'next/router';

const handleRouteChange = (url) => {
  // handling route change
  return `handling url - ${url}`;
};

const Component: FunctionComponent = () => {
  const router = useRouter();

  useEffect(() => {
    router.events.on('routeChangeComplete', handleRouteChange);
    return () => {
      router.events.off('routeChangeComplete', handleRouteChange);
    };
  }, [router.events]);

  // doing other things
};

and doing the following in my test to mock useRouter:

jest.mock('next/router');

let eventName;
let routeChangeHandler;

useRouter.mockImplementation(() => {
  return {
    events: {
      on: jest.fn((event, callback) => {
        eventName = event;
        routeChangeHandler = callback;
      }),
      off: jest.fn((event, callback) => {
        eventName = event;
        routeChangeHandler = callback;
      }),
    },
  };
});

and my test looks like (using react testing library)

it('should call the required functions', () => {
    render(<Component />);

    expect(useRouter).toHaveBeenCalledTimes(1);
    expect(eventName).toBe('routeChangeComplete'); 
    expect(routeChangeHandler).toBeDefined();

    expect(routeChangeHandler('/')).toBe('handling url - /');

    useRouter().events.on('onEvent', routeChangeHandler);
    expect(useRouter).toHaveBeenCalledTimes(2);
    expect(eventName).toBe('onEvent');

    useRouter().events.off('offEvent', routeChangeHandler);
    expect(useRouter).toHaveBeenCalledTimes(3);
    expect(eventName).toBe('offEvent');
});

@rafaelguedes
Copy link

rafaelguedes commented Feb 17, 2021

Not sure if I should drop this question here (sorry if this will cause "noise" in this issue).

When I'm testing the <Link /> behavior, expecting it to redirect to a certain route, there's a TypeError (Cannot read property 'push' of null).

import React from "react"
import Link from "next/link"

const Sandbox = () => {
  return (
    <div>
      <Link href="/about">
        <a data-testid="mytest">Click Me</a>
      </Link>
    </div>
  )
}

export default Sandbox
import React from "react"
import { render, fireEvent } from "@testing-library/react"
import { useRouter } from "next/router"
import Sandbox from ".."

jest.mock("next/router", () => ({
  useRouter: jest.fn(),
}))

describe("Sandbox", () => {
  it.only("should navigate accordingly", () => {
    const push = jest.fn()
    useRouter.mockImplementationOnce(() => ({
      asPath: "/",
      push,
    }))

    const { getByTestId } = render(<Sandbox />)

    const mytest = getByTestId("mytest")
    fireEvent.click(mytest)
    expect(push).toHaveBeenCalledWith("/about")
  })
})

I believe that I've mocked everything I need, so I don't really understand why the router can't actually "push".
I'm quite sure that this has to do with <Link /> since I can make the test pass if I use an anchor tag with router.push("/about").
What am I missing here?

@suvasishm
Copy link

suvasishm commented Feb 17, 2021

@rafaelguedes - The issue Cannot read property 'push' of null will be resolved if you do mock next/link:

jest.mock('next/link', () => ({ children }) => children);

But then test will fail at expect(push).toHaveBeenCalledWith("/about") as push is not being called at all. Maybe you need to mock the class Router (import { Router } from 'next/router';)?

@rafaelguedes
Copy link

@rafaelguedes - The issue Cannot read property 'push' of null will be resolved if you do mock next/link:

jest.mock('next/link', () => ({ children }) => children);

But then test will fail at expect(push).toHaveBeenCalledWith("/about") as push is not being called at all. Maybe you need to mock the class Router (import { Router } from 'next/router';)?

Thanks, @suvasishm.
I've already tried to mock Link, which actually makes sense, but I end up with the test failing again due to push not being called. I will try to mock Router, even though I'm not really sure how can I do it properly.

@linconkusunoki
Copy link

In my case, I just have a function that calls router.push, and I wanted to check this push.

For this, I mocked useRouter as mentioned by @aeksco

const useRouter = jest.spyOn(require('next/router'), 'useRouter')

And inside my test case

const router = { push: jest.fn() }
useRouter.mockReturnValue(router)
expect(router.push).toHaveBeenCalledWith('/path')

@jmhungdev
Copy link

jmhungdev commented Mar 10, 2021

After following @linconkusunoki solution, in case you encounter errors:
Cannot read property 'then' of undefined

try

const router = { push: jest.fn().mockImplementation(() => Promise.resolve()) };

Current implementation [email protected] of /client/link.tsx when .push is triggered
it will call three arguments href, as, { shallow, locale, scroll }

so your expect should be:

expect(router.push).toHaveBeenCalledWith('/path, '/path', expect.anything());

@Poky85
Copy link

Poky85 commented Mar 12, 2021

@rafaelguedes For testing <Link /> behavior you have to mock different module. In fact <Link /> depends on next/dist/client/router. Simple mock could look like:

jest.mock('next/dist/client/router', () => ({
  __esModule: true,
  useRouter: () => ({
    query: {},
    pathname: '/',
    asPath: '/',
    events: {
      emit: jest.fn(),
      on: jest.fn(),
      off: jest.fn(),
    },
    push: jest.fn(() => Promise.resolve(true)),
    prefetch: jest.fn(() => Promise.resolve(true)),
    replace: jest.fn(() => Promise.resolve(true)),
  }),
}))

@vercel vercel locked and limited conversation to collaborators Mar 13, 2021

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests