Next.js: Como simular useRouter?

Criado em 1 jun. 2019  ·  21Comentários  ·  Fonte: vercel/next.js

Pergunta sobre Next.js

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

Comentários muito úteis

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

Todos 21 comentários

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

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");
Esta página foi útil?
0 / 5 - 0 avaliações