Я хочу убедиться, что мой компонент правильно отображается с помощью хука useRouter (на самом деле я пытаюсь понять, как работает новая динамическая маршрутизация), поэтому у меня есть код:
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;
И я пытаюсь:
// 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));
});
Но я получаю сообщение об ошибке TypeError: Cannot read property 'query' of null
которое указывает на строку const router = useRouter();
.
PS Я знаю, что динамическая маршрутизация доступна на канареечных версиях только сейчас и может измениться, но у меня проблема с маршрутизатором, а не с функцией WIP (я?).
Привет, эта функция все еще экспериментальная, но useRouter
использует React.useContext
для использования контекста из next-server/dist/lib/router-context
. Чтобы издеваться над ним, вам нужно будет обернуть его в провайдере контекста оттуда, как в этой строке
@ijjk Привет, спасибо!
Не знаю, правильно ли я делаю, но тест проходит 😂
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));
});
Если есть более абстрактный способ имитировать параметры запроса, чтобы я мог передать фактический маршрут (например, /users/nikita
) и передать путь к файлу? Что вы думаете?
Лучше всего смоделировать маршрутизатор напрямую, а не вызывать createRouter
поскольку этот API является внутренним и может измениться в любое время. Вот пример:
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, что имеет смысл. Большое спасибо!
Есть ли способ имитировать useRouter с помощью Enzyme + Jest? Я немного искал в Интернете, и единственные релевантные результаты, которые появляются, - это эта проблема.
Мне удалось так поиздеваться над этим.
import * as nextRouter from 'next/router';
nextRouter.useRouter = jest.fn();
nextRouter.useRouter.mockImplementation(() => ({ route: '/' }));
jest.spyOn
у меня тоже работает -
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()
})
})
Я закончил тем, что издевался над этим, мне нужен только экспорт useRouter
так что он работал достаточно хорошо для моих целей:
jest.mock("next/router", () => ({
useRouter() {
return {
route: "/",
pathname: "",
query: "",
asPath: "",
};
},
}));
Если кто-то хочет издеваться над useRouter
просто чтобы избежать вмешательства со стороны императивного prefetch
, то этот мертвый простой макет будет работать.
jest.mock("next/router", () => ({
useRouter() {
return {
prefetch: () => null
};
}
}));
Примером использования может быть компонент формы, который включает что-то вроде:
const router = useRouter();
useEffect(() => {
router.prefetch("/success");
if (confirmSuccess) {
doStuff();
router.push( {pathname: "/success" } )
}
}, [data]);
@ijjk Изменилось ли это поведение в последней версии? Пришлось импортировать из next/dist/next-server/lib/router-context
. Он не распознал бы контекст, если бы я установил next-server
отдельно.
У меня такая же проблема.
Мы под следующим 9. Ни одно из решений, использующих RouterContext.Provider
самом деле не работает.
Единственный способ пройти тест - использовать решение @aeksco в качестве глобального объекта над тестом. В противном случае useRouter
всегда не определено.
Это не идеально, поскольку я не могу устанавливать другие параметры для своего теста.
Есть идеи по этому поводу?
РЕДАКТИРОВАТЬ
Я сделал это работать с глобальным издеваться в next/router
импорта и spyOn
на макет, который позволяет мне назвать mockImplementation(() => ({// whatever you want})
в каждом тесте.
Это выглядит примерно так:
jest.mock("next/router", () => ({
useRouter() {
return {
route: "",
pathname: "",
query: "",
asPath: "",
};
},
}));
const useRouter = jest.spyOn(require("next/router"), "useRouter");
Тогда в тестах:
useRouter.mockImplementation(() => ({
route: "/yourRoute",
pathname: "/yourRoute",
query: "",
asPath: "",
}));
Это не идеально, но, по крайней мере, для меня это работает
FWIW это то, на чем я остановился:
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
Мне нужно было что-то, что работало как в Storybook, так и в Jest. Кажется, это помогает, вы просто устанавливаете <Routermock>
где-нибудь в дереве компонентов. Это не идеально, потому что я не люблю постоянно переопределять Router.router
.
Я думаю, что официальное решение для насмешек было бы прекрасным :)
Метод @smasontst сработал для нас, но будьте осторожны с mockImplementationOnce()
... если вашему компоненту нужно отрендерить более одного раза во время вашего теста, вы обнаружите, что он не использует ваш макет роутера во втором рендере и ваш тест не удастся. Вероятно, лучше всегда использовать вместо этого mockImplementation()
, если только у вас нет особой причины использовать mockImplementationOnce()
.
Мне пришлось пересмотреть свою первоначальную реализацию, так как мне нужно было уникальное состояние useRouter
для каждого теста. Взял страницу из примера, предоставленного @ nterol24s, и обновил ее, чтобы она действовала как служебная функция, которую я могу вызывать в своих тестах:
// 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,
}));
}
Теперь я могу делать такие вещи, как:
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();
});
});
Обратите внимание на комментарий @mbrowne - вы mockNextUseRouter
и mockNextUseRouterOnce
если вам нужно.
Также БОЛЬШОЙ: +1: за официальное издевательское решение @timneutkens
Для тех, кто хочет глобально издеваться над экземпляром Router
, вы можете разместить в любом месте папку __mocks__
и настроить таргетинг на пакет next/router
следующим образом:
__mocks__/next/router/index.js
(должен соответствовать этому шаблону структуры папок!)
В этом примере ниже нацелены на Router.push
и 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");
Теперь везде, где есть import Router from "next/router";
это будет фиктивный экземпляр. Вы также сможете добавить к ним функции mockImplementation
поскольку они будут глобально издеваться.
Если вы хотите, чтобы этот экземпляр сбрасывался для каждого теста, тогда в вашем jest.json
добавьте свойство clearMocks .
Для справки, вот структура Router
если вы хотите настроить таргетинг на конкретный экспорт:
{
__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
}
}
Кроме того, если вам нужно mount
компоненты, которые используют withRouter
или useRouter
и вы не хотите издеваться над ними, но все же хотите создать несколько тестов против / вокруг их, то вы можете использовать эту фабричную функцию HOC-оболочки для тестирования:
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
*
* <strong i="33">@function</strong> withRouterContext
* <strong i="34">@param</strong> {node} Component - Component to be mounted
* <strong i="35">@param</strong> {object} initialProps - Component initial props for setup.
* <strong i="36">@param</strong> {object} state - Component initial state for setup.
* <strong i="37">@param</strong> {object} router - Initial route options for RouterContext.
* <strong i="38">@param</strong> {object} options - Optional options for enzyme's mount function.
* <strong i="39">@function</strong> createElement - Creates a wrapper around passed in component (now we can use wrapper.setProps on root)
* <strong i="40">@returns</strong> {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;
};
Пример использования:
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
Зачем это использовать? Потому что он позволяет вам иметь повторно используемый компонент React, обернутый в контекст маршрутизатора; и, что наиболее важно, он позволяет вызывать wrapper.setProps(..)
в корневом компоненте!
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')
})
})
Ни одно из этих решений не помогло мне. «Правильный» рабочий процесс также описан здесь, в документации Jest: https://jestjs.io/docs/en/es6-class-mocks#spying -on-methods-of-our-class.
Тем не менее, я вижу макет, но он не записывает звонки ...
Вот мой текущий test-utils.tsx
. Мне это нравится намного больше, чем использование глобального макета.
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 спасибо! Работает отлично!
Решение @flybayer работает для меня, однако я должен указать тип возвращаемого значения для функции рендеринга
import { render as defaultRender, RenderResult } from '@testing-library/react'
...
export function render(
ui: RenderUI,
{ wrapper, router, ...options }: RenderOptions = {}
): RenderResult { ... }
Для тех, кто хочет глобально издеваться над экземпляром
Router
, вы можете разместить в любом месте папку__mocks__
и настроить таргетинг на пакетnext/router
следующим образом:
__mocks__/next/router/index.js
(должен соответствовать этому шаблону структуры папок!)В этом примере ниже нацелены на
Router.push
и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");
Теперь везде, где есть
import Router from "next/router";
это будет фиктивный экземпляр. Вы также сможете добавить к ним функцииmockImplementation
поскольку они будут глобально издеваться.
Если вы хотите, чтобы этот экземпляр сбрасывался для каждого теста, тогда в вашемjest.json
добавьте свойство clearMocks .Для справки, вот структура
Router
если вы хотите настроить таргетинг на конкретный экспорт:{ __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 } }
Кроме того, если вам нужно
mount
компоненты, которые используютwithRouter
илиuseRouter
и вы не хотите издеваться над ними, но все же хотите создать несколько тестов против / вокруг их, то вы можете использовать эту фабричную функцию HOC-оболочки для тестирования: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 * * <strong i="33">@function</strong> withRouterContext * <strong i="34">@param</strong> {node} Component - Component to be mounted * <strong i="35">@param</strong> {object} initialProps - Component initial props for setup. * <strong i="36">@param</strong> {object} state - Component initial state for setup. * <strong i="37">@param</strong> {object} router - Initial route options for RouterContext. * <strong i="38">@param</strong> {object} options - Optional options for enzyme's mount function. * <strong i="39">@function</strong> createElement - Creates a wrapper around passed in component (now we can use wrapper.setProps on root) * <strong i="40">@returns</strong> {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; };
Пример использования:
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
Зачем это использовать? Потому что он позволяет вам иметь повторно используемый компонент React, обернутый в контекст маршрутизатора; и, что наиболее важно, он позволяет вызывать
wrapper.setProps(..)
в корневом компоненте!
привет, у меня такая ошибка:
TypeError: require.requireMock не является функцией
ИСПОЛЬЗОВАЛ ДАННОЕ РЕШЕНИЕ:
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");
Самый полезный комментарий
Я закончил тем, что издевался над этим, мне нужен только экспорт
useRouter
так что он работал достаточно хорошо для моих целей: