Next.js: ¿Cómo simular useRouter?

Creado en 1 jun. 2019  ·  21Comentarios  ·  Fuente: vercel/next.js

Pregunta sobre Next.js

Quiero asegurarme de que mi componente se procese correctamente con useRouter hook (en realidad, estoy tratando de entender cómo funciona el nuevo enrutamiento dinámico), así que tengo el 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;

Y lo que estoy intentando es:

// 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));
});

Pero obtengo un error TypeError: Cannot read property 'query' of null que apunta a la línea const router = useRouter(); .

PD: Sé que el enrutamiento dinámico está disponible en las versiones de Canary solo por ahora y podría cambiar, pero tengo un problema con el enrutador, no con la función WIP (¿verdad?).

Comentario más útil

Terminé burlándome de esto así, solo necesito la exportación useRouter así que esto funcionó lo suficientemente bien para mis propósitos:

jest.mock("next/router", () => ({
    useRouter() {
        return {
            route: "/",
            pathname: "",
            query: "",
            asPath: "",
        };
    },
}));

Todos 21 comentarios

Hola, esta función aún es experimental, pero useRouter usa React.useContext para consumir el contexto de next-server/dist/lib/router-context . Para simularlo, necesitaría envolverlo en el proveedor de contexto desde allí de manera similar a esta línea

@ijjk ¡ Hola, gracias!
No sé si lo estoy haciendo bien, pero la prueba pasa 😂

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));
});

Si hay una forma más abstracta de simular los parámetros de consulta, ¿podría pasar la ruta real ( /users/nikita por ejemplo) y pasar la ruta al archivo? ¿Qué piensas?

Podría ser mejor burlarse del enrutador directamente en lugar de llamar a createRouter ya que esa API es interna y puede cambiar en cualquier momento. He aquí un ejemplo:

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 eso tiene sentido. ¡Muchas gracias!

¿Hay alguna forma de simular useRouter usando Enzyme + Jest? He estado buscando en línea durante un tiempo y el único resultado relevante que aparece es este problema.

Me las arreglé para burlarme de ello de esta manera.

import * as nextRouter from 'next/router';

nextRouter.useRouter = jest.fn();
nextRouter.useRouter.mockImplementation(() => ({ route: '/' }));

jest.spyOn también me funciona -

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()
  })
})

Terminé burlándome de esto así, solo necesito la exportación useRouter así que esto funcionó lo suficientemente bien para mis propósitos:

jest.mock("next/router", () => ({
    useRouter() {
        return {
            route: "/",
            pathname: "",
            query: "",
            asPath: "",
        };
    },
}));

Si alguien está aquí buscando burlarse de useRouter simplemente para evitar la interferencia de un prefetch imperativo, entonces esta simple simulación funcionará.

jest.mock("next/router", () => ({
  useRouter() {
    return {
      prefetch: () => null
    };
  }
}));

un caso de uso de ejemplo sería un componente de formulario que incluye algo como:

  const router = useRouter();
  useEffect(() => {
    router.prefetch("/success");
    if (confirmSuccess) {
      doStuff();
      router.push( {pathname: "/success" } )
    }
  }, [data]);

@ijjk ¿Ha cambiado ese comportamiento en la última versión? Tuve que importar desde next/dist/next-server/lib/router-context . No reconocería el contexto si instalara next-server separado.

Tengo exactamente el mismo problema.
Estamos por debajo de los siguientes 9. Ninguna de las soluciones que usan RouterContext.Provider realmente funciona.
La única forma en que mi prueba pasa es usando la solución @aeksco como un objeto global por encima de la prueba. De lo contrario, useRouter siempre está indefinido.
Esto no es ideal ya que no puedo establecer diferentes parámetros para mi prueba.
¿Alguna idea sobre esto?

EDITAR
Lo hice funcionar con una simulación global de la importación next/router y una spyOn en la simulación, lo que me permite llamar a mockImplementation(() => ({// whatever you want}) en cada prueba.
Se parece a algo como:

jest.mock("next/router", () => ({
  useRouter() {
    return {
      route: "",
      pathname: "",
      query: "",
      asPath: "",
    };
  },
}));

const useRouter = jest.spyOn(require("next/router"), "useRouter");

Luego en las pruebas:

useRouter.mockImplementation(() => ({
      route: "/yourRoute",
      pathname: "/yourRoute",
      query: "",
      asPath: "",
    }));

Esto no es ideal, pero al menos funciona para mí.

FWIW, esto es en lo que me he decidido:

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

Necesitaba algo que funcionara tanto en Storybook como en Jest. Esto parece funcionar, simplemente establece <Routermock> algún lugar del árbol de componentes. No es ideal porque no me encanta anular Router.router constantemente.

Creo que una solución oficial de burla sería encantadora :)

El método de @smasontst funcionó para nosotros, pero tenga cuidado con mockImplementationOnce() ... si su componente necesita renderizarse más de una vez durante su prueba, encontrará que no está usando su enrutador simulado en el segundo render y tu prueba fallará. Probablemente sea mejor usar siempre mockImplementation() lugar, a menos que tenga una razón específica para usar mockImplementationOnce() .

Tuve que revisar mi implementación inicial ya que necesitaba un estado único useRouter prueba por prueba. Tomé una página del ejemplo proporcionado por @ nterol24s y la actualicé para que actúe como una función de utilidad a la que puedo llamar dentro de mis pruebas:

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

Ahora puedo hacer cosas 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();
    });
});

Tenga en cuenta el comentario de @mbrowne : encontrará el mismo problema con este enfoque, pero puede dividir el ejemplo anterior en funciones mockNextUseRouter y mockNextUseRouterOnce si lo necesita.

También un GRAN: +1: para una solución oficial de burla @timneutkens

Para cualquiera que quiera una instancia Router simulada globalmente , puede colocar una carpeta __mocks__ cualquier lugar y apuntar al paquete next/router así:

__mocks__/next/router/index.js (¡debe seguir este patrón de estructura de carpetas!)

Este ejemplo a continuación apunta a Router.push y 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");

Ahora, en cualquier lugar donde haya un import Router from "next/router"; será la instancia simulada. También podrá agregar funciones mockImplementation en ellos, ya que serán burladas globalmente.
Si desea que esta instancia se restablezca para cada prueba, en su jest.json agregue una propiedad clearMocks .

Como referencia, aquí está la estructura Router si desea apuntar a una exportación 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 
  }
}

Además, si tiene que mount componentes que utilizan withRouter o useRouter y no quiere burlarse de ellos, pero aún desea crear algunas pruebas contra / alrededor ellos, entonces puede utilizar esta función de fábrica de envoltorios HOC para probar:

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;
};

Uso de ejemplo:

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 qué usar esto? Porque le permite tener un componente React montado reutilizable envuelto en un contexto de enrutador; y lo más importante, ¡le permite llamar a wrapper.setProps(..) en el componente raíz!

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')
  })
})

Ninguna de estas soluciones funcionó para mí. El flujo de trabajo "correcto" también se describe aquí en los documentos de Jest: https://jestjs.io/docs/en/es6-class-mocks#spying -on-methods-of-our-class

Sin embargo, puedo ver el simulacro, pero no graba llamadas ...

Aquí está mi test-utils.tsx actual. Me gusta mucho más esto que usar una simulación 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 gracias! ¡Funciona genial!

La solución de @flybayer funciona para mí, sin embargo, tengo que especificar el tipo de retorno en la función de renderizado

import { render as defaultRender, RenderResult } from '@testing-library/react'

...

export function render(
  ui: RenderUI,
  { wrapper, router, ...options }: RenderOptions = {}
): RenderResult { ... }

Para cualquiera que quiera una instancia Router simulada globalmente , puede colocar una carpeta __mocks__ cualquier lugar y apuntar al paquete next/router así:

__mocks__/next/router/index.js (¡debe seguir este patrón de estructura de carpetas!)

Este ejemplo a continuación apunta a Router.push y 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");

Ahora, en cualquier lugar donde haya un import Router from "next/router"; será la instancia simulada. También podrá agregar funciones mockImplementation en ellos, ya que serán burladas globalmente.
Si desea que esta instancia se restablezca para cada prueba, en su jest.json agregue una propiedad clearMocks .

Como referencia, aquí está la estructura Router si desea apuntar a una exportación 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 
  }
}

Además, si tiene que mount componentes que utilizan withRouter o useRouter y no quiere burlarse de ellos, pero aún desea crear algunas pruebas contra / alrededor ellos, entonces puede utilizar esta función de fábrica de envoltorios HOC para probar:

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;
};

Uso de ejemplo:

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 qué usar esto? Porque le permite tener un componente React montado reutilizable envuelto en un contexto de enrutador; y lo más importante, ¡le permite llamar a wrapper.setProps(..) en el componente raíz!

hola, recibo este error:

TypeError: require.requireMock no es una función

UTILIZÓ ESTA SOLUCIÓN:

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");
¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

havefive picture havefive  ·  3Comentarios

ghost picture ghost  ·  3Comentarios

formula349 picture formula349  ·  3Comentarios

kenji4569 picture kenji4569  ·  3Comentarios

jesselee34 picture jesselee34  ·  3Comentarios