Quero ter certeza de que meu componente é renderizado corretamente com o gancho useRouter (na verdade, estou tentando entender como funciona o novo roteamento dinâmico), então tenho o código:
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;
E o que estou tentando é:
// 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));
});
Mas recebo um erro TypeError: Cannot read property 'query' of null
que aponta para a linha const router = useRouter();
.
PS Sei que o roteamento dinâmico está disponível nas versões canário por enquanto e pode mudar, mas tenho um problema com o roteador, não com o recurso WIP (estou?).
Olá, este recurso ainda é experimental, mas useRouter
usa React.useContext
para consumir o contexto de next-server/dist/lib/router-context
. Para simular, você precisa envolvê-lo no provedor de contexto de lá, semelhante a esta linha
@ijjk Oi, obrigado!
Não sei se estou fazendo certo, mas passa no teste 😂
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));
});
Se houver uma maneira mais abstrata de simular parâmetros de consulta, eu seria capaz de passar a rota real ( /users/nikita
por exemplo) e passar o caminho para o arquivo? O que você acha?
Pode ser melhor simular o roteador diretamente em vez de chamar createRouter
pois essa API é interna e pode ser alterada a qualquer momento. Aqui está um exemplo:
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 isso faz sentido. Muito obrigado!
Existe alguma maneira de simular o useRouter usando Enzyme + Jest? Estive pesquisando online um pouco e os únicos resultados relevantes que surgiram é esse problema.
Eu consegui zombar dessa maneira.
import * as nextRouter from 'next/router';
nextRouter.useRouter = jest.fn();
nextRouter.useRouter.mockImplementation(() => ({ route: '/' }));
jest.spyOn
funciona para mim -
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()
})
})
Acabei zombando disso assim, só preciso da exportação useRouter
então funcionou bem o suficiente para meus objetivos:
jest.mock("next/router", () => ({
useRouter() {
return {
route: "/",
pathname: "",
query: "",
asPath: "",
};
},
}));
Se alguém está aqui procurando simular useRouter
simplesmente para evitar a interferência de um prefetch
imperativo, então este simulado simples funcionará
jest.mock("next/router", () => ({
useRouter() {
return {
prefetch: () => null
};
}
}));
um exemplo de caso de uso seria um componente de formulário que inclui algo como:
const router = useRouter();
useEffect(() => {
router.prefetch("/success");
if (confirmSuccess) {
doStuff();
router.push( {pathname: "/success" } )
}
}, [data]);
@ijjk Esse comportamento mudou na versão mais recente? Tive que importar de next/dist/next-server/lib/router-context
. Ele não reconheceria o contexto se eu instalasse next-server
separadamente.
Eu tenho exatamente o mesmo problema.
Estamos nos próximos 9. Nenhuma das soluções que usam RouterContext.Provider
realmente funciona.
A única maneira de minha aprovação no teste é usar a solução @aeksco como um objeto global acima do teste. Caso contrário, useRouter
é sempre indefinido.
Isso não é ideal, pois não posso definir parâmetros diferentes para o meu teste.
Alguma ideia sobre isso?
EDITAR
Fiz funcionar com um mock global de next/router
import e spyOn
no mock, o que me permite chamar mockImplementation(() => ({// whatever you want})
em cada teste.
É algo como:
jest.mock("next/router", () => ({
useRouter() {
return {
route: "",
pathname: "",
query: "",
asPath: "",
};
},
}));
const useRouter = jest.spyOn(require("next/router"), "useRouter");
Então, nos testes:
useRouter.mockImplementation(() => ({
route: "/yourRoute",
pathname: "/yourRoute",
query: "",
asPath: "",
}));
Isso não é ideal, mas pelo menos funciona para mim
FWIW é o que eu decidi:
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
Eu precisava de algo que funcionasse tanto no Storybook quanto no Jest. Isso parece funcionar, basta definir <Routermock>
algum lugar na árvore de componentes. Não é o ideal porque não adoro substituir Router.router
constantemente.
Acho que uma solução oficial de mocking seria ótima :)
O método de @smasontst funcionou para nós, mas tenha cuidado com mockImplementationOnce()
... se o seu componente precisar renderizar mais de uma vez durante o teste, você descobrirá que não está usando seu roteador simulado na segunda renderização e seu teste falhará. Provavelmente, é melhor sempre usar mockImplementation()
, a menos que você tenha um motivo específico para usar mockImplementationOnce()
.
Tive que revisar minha implementação inicial, pois precisava de um estado useRouter
exclusivo em uma base teste a teste. Peguei uma página do exemplo fornecido por @ nterol24s e atualizei para atuar como uma função de utilidade que posso chamar em meus testes:
// 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,
}));
}
Agora posso fazer coisas como:
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();
});
});
Observe o comentário de @mbrowne - você terá o mesmo problema com essa abordagem, mas pode dividir o exemplo acima nas funções mockNextUseRouter
e mockNextUseRouterOnce
, se necessário.
Também um GRANDE: +1: para uma solução oficial de mocking @timneutkens
Para quem deseja uma instância Router
simulada globalmente , você pode colocar uma pasta __mocks__
qualquer lugar e direcionar o pacote next/router
maneira:
__mocks__/next/router/index.js
(tem que seguir este padrão de estrutura de pasta!)
Este exemplo abaixo visa Router.push
e 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");
Agora, em qualquer lugar onde houver um import Router from "next/router";
, será a instância simulada. Você também poderá adicionar mockImplementation
funções neles, já que serão simulados globalmente.
Se você deseja que esta instância seja redefinida para cada teste, em seu jest.json
adicione uma propriedade clearMocks .
Para referência, aqui está a estrutura Router
se você deseja direcionar uma exportação específica:
{
__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
}
}
Além disso, se você tiver que mount
componentes que utilizam withRouter
ou useRouter
e não quiser zombar deles, mas ainda quiser criar alguns testes contra / ao redor eles, então você pode utilizar esta função de fábrica de invólucro HOC para testar:
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;
};
Exemplo de uso:
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
Por que usar isso? Porque ele permite que você tenha um componente React montado reutilizável envolvido em um contexto de Roteador; e o mais importante, permite que você chame wrapper.setProps(..)
no componente raiz!
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')
})
})
Nenhuma dessas soluções funcionou para mim. O fluxo de trabalho "correto" também é descrito aqui nos documentos do Jest: https://jestjs.io/docs/en/es6-class-mocks#spying -on-methods-of-our-class
Porém, eu posso ver o mock, mas ele não grava chamadas ...
Aqui está meu test-utils.tsx
atual. Eu gosto disso muito mais do que usar um mock global.
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 obrigado! Funciona bem!
A solução de @flybayer funciona para mim, no entanto, tenho que especificar o tipo de retorno na função de renderização
import { render as defaultRender, RenderResult } from '@testing-library/react'
...
export function render(
ui: RenderUI,
{ wrapper, router, ...options }: RenderOptions = {}
): RenderResult { ... }
Para quem deseja uma instância
Router
simulada globalmente , você pode colocar uma pasta__mocks__
qualquer lugar e direcionar o pacotenext/router
maneira:
__mocks__/next/router/index.js
(tem que seguir este padrão de estrutura de pasta!)Este exemplo abaixo visa
Router.push
eRouter.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");
Agora, em qualquer lugar onde houver um
import Router from "next/router";
, será a instância simulada. Você também poderá adicionarmockImplementation
funções neles, já que serão simulados globalmente.
Se você deseja que esta instância seja redefinida para cada teste, em seujest.json
adicione uma propriedade clearMocks .Para referência, aqui está a estrutura
Router
se você deseja direcionar uma exportação específica:{ __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 } }
Além disso, se você tiver que
mount
componentes que utilizamwithRouter
ouuseRouter
e não quiser zombar deles, mas ainda quiser criar alguns testes contra / ao redor eles, então você pode utilizar esta função de fábrica de invólucro HOC para testar: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; };
Exemplo de uso:
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
Por que usar isso? Porque ele permite que você tenha um componente React montado reutilizável envolvido em um contexto de Roteador; e o mais importante, permite que você chame
wrapper.setProps(..)
no componente raiz!
oi, estou recebendo este erro:
TypeError: require.requireMock não é uma função
USADO ESTA SOLUÇÃO:
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");
Comentários muito úteis
Acabei zombando disso assim, só preciso da exportação
useRouter
então funcionou bem o suficiente para meus objetivos: