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?).
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 thenext/router
package like so:
__mocks__/next/router/index.js
(has to follow this folder structure pattern!)This example below targets
Router.push
andRouter.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 addmockImplementation
functions on them since they'll be globally mocked.
If you want this instance to be reset for each test, then in yourjest.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 utilizewithRouter
oruseRouter
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");
Most helpful comment
I ended up mocking it like this, I only need the
useRouter
export so this worked well enough for my purposes: