-
Notifications
You must be signed in to change notification settings - Fork 27k
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
Comments
Hi, this feature is still experimental but |
@ijjk Hi, thank you! 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 ( |
This comment has been minimized.
This comment has been minimized.
@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: '/' })); |
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 jest.mock("next/router", () => ({
useRouter() {
return {
route: "/",
pathname: "",
query: "",
asPath: "",
};
},
})); |
If anyone is here looking to mock
an example use case would be a form component that includes something like:
|
@ijjk Has that behaviour changed in the latest version? I had to import from |
I have the same exact problem. EDIT
Then in the tests :
This is not ideal but at least it works for me |
FWIW this is what I've settled on:
I needed something that worked both in Storybook and in Jest. This seems to do the trick, you just set I think an official mocking solution would be lovely :) |
@smasontst's method worked for us, but be careful with |
I had to revise my initial implementation since I needed unique // 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 Also a BIG 👍 for an official mocking solution @timneutkens |
For anyone who wants a globally mocked
This example below targets 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 For reference, here's the {
__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 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 |
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 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
|
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"); |
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. |
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:
|
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;
}; |
@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> |
@mbrowne that's 🔥! Thanks for the refactor, updated! |
When using the approach of @flybayer , I encountered a weird error message:
Seems like, next tries to 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(),
},
}; |
im work with on click event on link use next/router hook "useRoute"
and fix whit 1.- global mock router next js in "mocks\next\router\index.js" file
`
}); ` |
Thanks this worked perfectly!!! 🍰 |
ESLint rule may complain about directly reassigning "read-only" imports due to
Its possibly better to use jest's mock or custom render as suggested above. |
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"); |
My component using 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 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');
}); |
Not sure if I should drop this question here (sorry if this will cause "noise" in this issue). When I'm testing the
I believe that I've mocked everything I need, so I don't really understand why the router can't actually "push". |
@rafaelguedes - The issue jest.mock('next/link', () => ({ children }) => children); But then test will fail at |
Thanks, @suvasishm. |
In my case, I just have a function that calls 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') |
After following @linconkusunoki solution, in case you encounter errors: try const router = { push: jest.fn().mockImplementation(() => Promise.resolve()) }; Current implementation [email protected] of so your expect should be: expect(router.push).toHaveBeenCalledWith('/path, '/path', expect.anything()); |
@rafaelguedes For testing
|
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
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:
And what I'm trying is:
But I get an error
TypeError: Cannot read property 'query' of null
which points onconst 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?).
The text was updated successfully, but these errors were encountered: