Next.js: Comment se moquer de useRouter?

Créé le 1 juin 2019  ·  21Commentaires  ·  Source: vercel/next.js

Question sur Next.js

Je veux m'assurer que mon composant s'affiche correctement avec le hook useRouter (en fait, j'essaie de comprendre comment fonctionne le nouveau routage dynamique), j'ai donc le 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;

Et ce que j'essaye c'est :

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

Mais j'obtiens une erreur TypeError: Cannot read property 'query' of null qui pointe sur la ligne const router = useRouter(); .

PS Je sais que le routage dynamique est disponible sur les versions Canary pour le moment et pourrait changer, mais j'ai un problème avec le routeur, pas avec la fonction WIP (suis-je ?).

Commentaire le plus utile

J'ai fini par me moquer comme ça, je n'ai besoin que de l'export useRouter donc cela a assez bien fonctionné pour mes besoins :

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

Tous les 21 commentaires

Bonjour, cette fonctionnalité est encore expérimentale mais useRouter utilise React.useContext pour consommer le contexte de next-server/dist/lib/router-context . Pour vous en moquer, vous devrez l'envelopper dans le fournisseur de contexte similaire à cette ligne

@ijjk Salut, merci !
Je ne sais pas si je le fais bien, mais le test passe

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

S'il existe un moyen plus abstrait de simuler les paramètres de requête, je pourrais donc passer la route réelle ( /users/nikita par exemple) et passer le chemin d'accès au fichier ? Qu'est-ce que tu penses?

Il peut être préférable de se moquer directement du routeur au lieu d'appeler createRouter car cette API est interne et peut changer à tout moment. Voici un exemple :

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 qui a du sens. Merci beaucoup!

Existe-t-il un moyen de se moquer de useRouter en utilisant Enzyme + Jest ? J'ai cherché un peu en ligne et les seuls résultats pertinents qui se présentent sont ce problème.

J'ai réussi à m'en moquer de cette façon.

import * as nextRouter from 'next/router';

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

jest.spyOn fonctionne pour moi aussi -

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

J'ai fini par me moquer comme ça, je n'ai besoin que de l'export useRouter donc cela a assez bien fonctionné pour mes besoins :

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

Si quelqu'un cherche à se moquer de useRouter simplement pour éviter les interférences d'un impératif prefetch , alors cette simple simulation fonctionnera

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

un exemple de cas d'utilisation serait un composant de formulaire qui inclut quelque chose comme :

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

@ijjk Ce comportement a-t-il changé dans la dernière version ? J'ai dû importer à partir de next/dist/next-server/lib/router-context . Il ne reconnaîtrait pas le contexte si j'installais next-server séparément.

J'ai exactement le même problème.
Nous sommes sous 9. Aucune des solutions utilisant le RouterContext.Provider ne fonctionne réellement.
La seule façon dont mon test réussit est d'utiliser la solution useRouter est toujours indéfini.
Ce n'est pas idéal car je ne peux pas définir différents paramètres pour mon test.
Des idées à ce sujet ?

ÉDITER
Je l'ai fait fonctionner avec un simulacre global de l'import next/router et un spyOn sur le simulacre, ce qui me permet d'appeler mockImplementation(() => ({// whatever you want}) à chaque test.
Cela ressemble à quelque chose comme :

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

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

Ensuite dans les tests :

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

Ce n'est pas l'idéal mais au moins ça marche pour moi

FWIW c'est ce que j'ai choisi:

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

J'avais besoin de quelque chose qui fonctionne à la fois dans Storybook et dans Jest. Cela semble faire l'affaire, vous venez de définir <Routermock> quelque part dans l'arborescence des composants. Ce n'est pas idéal parce que je n'aime pas écraser Router.router permanence.

Je pense qu'une solution de moquerie officielle serait sympa :)

La méthode de @smasontst a fonctionné pour nous, mais soyez prudent avec mockImplementationOnce() ... si votre composant doit être rendu plus d'une fois pendant votre test, vous constaterez qu'il n'utilise pas votre faux routeur lors du deuxième rendu et votre test échouera. Il est probablement préférable de toujours utiliser mockImplementation() place, à moins que vous n'ayez une raison spécifique d'utiliser mockImplementationOnce() .

J'ai dû réviser mon implémentation initiale car j'avais besoin d'un état unique useRouter sur une base test par test. A pris une page de l'exemple fourni par @nterol24s et l'a mise à jour pour agir comme une fonction utilitaire que je peux appeler dans mes 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,
    }));
}

Je peux maintenant faire des choses comme :

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

Notez le commentaire de @mbrowne - vous rencontrerez le même problème avec cette approche, mais vous pouvez diviser l'exemple ci-dessus en fonctions mockNextUseRouter et mockNextUseRouterOnce si vous en avez besoin.

Aussi un GROS :+1: pour une solution de moquerie officielle @timneutkens

Pour tous ceux qui veulent une instance Router moquée globalement , vous pouvez placer un dossier __mocks__ n'importe où et cibler le package next/router comme suit :

__mocks__/next/router/index.js (doit suivre ce modèle de structure de dossier !)

Cet exemple ci-dessous cible Router.push et 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");

Maintenant, partout où il y a un import Router from "next/router"; ce sera l'instance simulée. Vous pourrez également leur ajouter des fonctions mockImplementation car elles seront globalement moquées.
Si vous souhaitez que cette instance soit réinitialisée pour chaque test, ajoutez une propriété clearMocks dans votre jest.json .

Pour référence, voici la structure Router si vous souhaitez cibler une exportation spécifique :

{
  __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 
  }
}

De plus, si vous devez mount composants qui utilisent withRouter ou useRouter et que vous ne voulez pas vous

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

Exemple d'utilisation :

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

Pourquoi utiliser ça ? Parce qu'il vous permet d'avoir un composant React monté réutilisable enveloppé dans un contexte de routeur ; et surtout, il vous permet d'appeler wrapper.setProps(..) sur le composant racine !

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

Aucune de ces solutions n'a fonctionné pour moi. Le workflow « correct » est également décrit ici dans les documents Jest : https://jestjs.io/docs/en/es6-class-mocks#spying -on-methods-of-our-class

Cependant, je peux voir la simulation, mais elle n'enregistre pas les appels...

Voici mon test-utils.tsx actuel. J'aime beaucoup mieux cela que d'utiliser une maquette globale.

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 merci ! Fonctionne très bien!

La solution de @flybayer fonctionne pour moi, mais je dois spécifier le type de retour sur la fonction de rendu

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

...

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

Pour tous ceux qui veulent une instance Router moquée globalement , vous pouvez placer un dossier __mocks__ n'importe où et cibler le package next/router comme suit :

__mocks__/next/router/index.js (doit suivre ce modèle de structure de dossier !)

Cet exemple ci-dessous cible Router.push et 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");

Maintenant, partout où il y a un import Router from "next/router"; ce sera l'instance simulée. Vous pourrez également leur ajouter des fonctions mockImplementation car elles seront globalement moquées.
Si vous souhaitez que cette instance soit réinitialisée pour chaque test, ajoutez une propriété clearMocks dans votre jest.json .

Pour référence, voici la structure Router si vous souhaitez cibler une exportation spécifique :

{
  __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 
  }
}

De plus, si vous devez mount composants qui utilisent withRouter ou useRouter et que vous ne voulez pas vous

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

Exemple d'utilisation :

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

Pourquoi utiliser ça ? Parce qu'il vous permet d'avoir un composant React monté réutilisable enveloppé dans un contexte de routeur ; et surtout, il vous permet d'appeler wrapper.setProps(..) sur le composant racine !

salut, j'obtiens cette erreur:

TypeError : require.requireMock n'est pas une fonction

UTILISÉ CETTE 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");
Cette page vous a été utile?
0 / 5 - 0 notes