Next.js: How to mock useRouter?

Created on 1 Jun 2019  ·  21Comments  ·  Source: vercel/next.js

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?).

Most helpful comment

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: "",
        };
    },
}));

All 21 comments

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 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?

It might be best to mock the router directly instead of calling createRouter since that API is internal and can change at any time. Here's an example:

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

describe('Basic test', () => {
  it('Renders current user value', async () => {
    const router = {
      pathname: '/users/$user',
      route: '/users/$user',
      query: { user: 'nikita' },
      asPath: '/users/nikita',
    }
    const User = require('../pages/users/$user').default
    const tree = render(
      <RouterContext.Provider value={router}>
         <User />
      </RouterContext.Provider>
    )
    expect(tree.getByText('User: nikita')).toBeTruthy()
  })
})

@ijjk that make sense. Thank you a lot!

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.

I managed to mock it this way.

import * as nextRouter from 'next/router';

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

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

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: "",
        };
    },
}));

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

@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.

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

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 :)

@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().

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 :+1: for an official mocking solution @timneutkens

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!

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

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...

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

@flybayer thanks! Works great!

@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 { ... }

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");
Was this page helpful?
0 / 5 - 0 ratings

Related issues

jesselee34 picture jesselee34  ·  3Comments

havefive picture havefive  ·  3Comments

formula349 picture formula349  ·  3Comments

sospedra picture sospedra  ·  3Comments

rauchg picture rauchg  ·  3Comments