Next.js: Wie verspotte ich useRouter?

Erstellt am 1. Juni 2019  ·  21Kommentare  ·  Quelle: vercel/next.js

Frage zu Next.js

Ich möchte sicherstellen, dass meine Komponente mit dem useRouter-Hook korrekt gerendert wird (eigentlich versuche ich zu verstehen, wie neues dynamisches Routing funktioniert), also habe ich 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;

Und was ich versuche ist:

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

Aber ich erhalte einen Fehler TypeError: Cannot read property 'query' of null der auf die Zeile const router = useRouter(); .

PS Ich weiß, dass dynamisches Routing derzeit nur auf Canary-Versionen verfügbar ist und sich möglicherweise ändern kann, aber ich habe ein Problem mit dem Router, nicht mit der WIP-Funktion (oder?).

Hilfreichster Kommentar

Am Ende habe ich es so verspottet, ich brauche nur den useRouter Export, damit das für meine Zwecke gut genug funktioniert:

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

Alle 21 Kommentare

Hallo, diese Funktion ist noch experimentell, aber useRouter verwendet React.useContext , um den Kontext von next-server/dist/lib/router-context zu konsumieren. Um es zu verspotten, müssten Sie es ähnlich wie in dieser Zeile in den Kontextanbieter einschließen

@ijjk Hallo, danke!
Ich weiß nicht, ob ich es richtig mache, aber Test bestanden 😂

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

Wenn es eine abstraktere Möglichkeit gibt, Abfrageparameter zu simulieren, damit ich die tatsächliche Route (z. B. /users/nikita ) und den Pfad zur Datei übergeben kann? Was denken Sie?

Es ist möglicherweise am besten, den Router direkt zu verspotten, anstatt createRouter aufzurufen, da diese API intern ist und sich jederzeit ändern kann. Hier ist ein Beispiel:

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 das macht Sinn. Vielen Dank!

Gibt es eine Möglichkeit, useRouter mit Enzyme+Jest zu verspotten? Ich habe ein bisschen im Internet gesucht und die einzigen relevanten Ergebnisse, die auftauchen, sind dieses Problem.

Ich habe es geschafft, es auf diese Weise zu verspotten.

import * as nextRouter from 'next/router';

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

jest.spyOn funktioniert bei mir auch -

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

Am Ende habe ich es so verspottet, ich brauche nur den useRouter Export, damit das für meine Zwecke gut genug funktioniert:

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

Wenn jemand hier ist, um useRouter zu verspotten, nur um Störungen durch ein Imperativ prefetch zu vermeiden, dann wird dieser tote einfache Schein funktionieren

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

ein Beispielanwendungsfall wäre eine Formularkomponente, die Folgendes enthält:

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

@ijjk Hat sich dieses Verhalten in der neuesten Version geändert? Ich musste von next/dist/next-server/lib/router-context importieren. Es würde den Kontext nicht erkennen, wenn ich next-server separat installiert hätte.

Ich habe genau das gleiche Problem.
Wir sind unter der nächsten 9. Keine der Lösungen, die RouterContext.Provider funktioniert tatsächlich.
Die einzige Möglichkeit, meinen Test zu bestehen, besteht darin, die @aeksco- Lösung als globales Objekt über dem Test zu verwenden. Ansonsten ist useRouter immer undefiniert.
Dies ist nicht ideal, da ich für meinen Test keine anderen Parameter einstellen kann.
Irgendwelche Ideen dazu?

BEARBEITEN
Ich habe es mit einem globalen Mock des next/router Imports und einem spyOn auf dem Mock zum Laufen gebracht, wodurch ich in jedem Test mockImplementation(() => ({// whatever you want}) aufrufen kann.
Es sieht ungefähr so ​​aus:

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

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

Dann in den Tests:

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

Das ist nicht ideal, aber zumindest funktioniert es bei mir

FWIW, das habe ich mir vorgenommen:

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

Ich brauchte etwas, das sowohl in Storybook als auch in Jest funktionierte. Dies scheint den Zweck zu erfüllen, Sie legen einfach <Routermock> irgendwo im Komponentenbaum fest. Es ist nicht ideal, weil ich es nicht liebe, Router.router ständig zu überschreiben.

Ich denke, eine offizielle Spottlösung wäre schön :)

Die Methode von @smasontst hat für uns funktioniert, aber seien Sie vorsichtig mit mockImplementationOnce() ... wenn Ihre Komponente während Ihres Tests mehr als einmal gerendert werden muss, werden Sie feststellen, dass sie beim zweiten Rendern nicht Ihren Mock-Router verwendet und Ihr Test wird fehlschlagen. Es ist wahrscheinlich am besten, stattdessen immer mockImplementation() verwenden, es sei denn, Sie haben einen bestimmten Grund, mockImplementationOnce() .

Ich musste meine anfängliche Implementierung überarbeiten, da ich von Test zu Test einen eindeutigen useRouter Zustand benötigte. Ich habe eine Seite aus dem von @nterol24s bereitgestellten Beispiel Hilfsfunktion zu fungieren, die ich in meinen Tests aufrufen kann:

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

Ich kann jetzt Dinge tun wie:

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

Beachten Sie den Kommentar von @mbrowne - Sie werden bei diesem Ansatz auf dasselbe Problem mockNextUseRouter und mockNextUseRouterOnce aufteilen.

Auch ein GROSSES :+1: für eine offizielle Spottlösung @timneutkens

Für jeden, der eine global verspottete Router Instanz möchte, können Sie einen __mocks__ Ordner an einer beliebigen Stelle platzieren und das Paket next/router wie folgt anvisieren:

__mocks__/next/router/index.js (muss diesem Muster der Ordnerstruktur folgen!)

Dieses Beispiel unten zielt auf Router.push und 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");

Überall dort, wo ein import Router from "next/router"; , wird es die verspottete Instanz sein. Sie können ihnen auch mockImplementation Funktionen hinzufügen, da sie global verspottet werden.
Wenn Sie möchten, dass diese Instanz für jeden Test zurückgesetzt wird, fügen Sie in Ihrem jest.json eine clearMocks- Eigenschaft hinzu.

Als Referenz hier die Router Struktur, wenn Sie auf einen bestimmten Export abzielen möchten:

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

Außerdem, wenn Sie mount Komponenten benötigen , die zufällig withRouter oder useRouter und Sie sie nicht verspotten möchten, aber dennoch einige Tests gegen/um erstellen möchten Sie können diese HOC-Wrapper-Factory-Funktion zum Testen verwenden:

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

Anwendungsbeispiel:

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

Warum das verwenden? Weil es Ihnen ermöglicht, eine wiederverwendbare gemountete React-Komponente in einen Router-Kontext einzuschließen; und am wichtigsten ist, dass Sie wrapper.setProps(..) für die Root-Komponente aufrufen können!

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

Keine dieser Lösungen hat bei mir funktioniert. Der "richtige" Workflow ist auch hier in den Jest-Dokumenten beschrieben: https://jestjs.io/docs/en/es6-class-mocks#spying -on-methods-of-our-class

Ich kann den Mock jedoch sehen, aber er zeichnet keine Anrufe auf...

Hier ist mein aktuelles test-utils.tsx . Das gefällt mir viel besser als die Verwendung eines globalen Mocks.

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 danke! Funktioniert super!

Die Lösung von @flybayer funktioniert für mich, ich muss jedoch den Rückgabetyp bei der

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

...

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

Für jeden, der eine global verspottete Router Instanz möchte, können Sie einen __mocks__ Ordner an einer beliebigen Stelle platzieren und das Paket next/router wie folgt anvisieren:

__mocks__/next/router/index.js (muss diesem Muster der Ordnerstruktur folgen!)

Dieses Beispiel unten zielt auf Router.push und 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");

Überall dort, wo ein import Router from "next/router"; , wird es die verspottete Instanz sein. Sie können ihnen auch mockImplementation Funktionen hinzufügen, da sie global verspottet werden.
Wenn Sie möchten, dass diese Instanz für jeden Test zurückgesetzt wird, fügen Sie in Ihrem jest.json eine clearMocks- Eigenschaft hinzu.

Als Referenz hier die Router Struktur, wenn Sie auf einen bestimmten Export abzielen möchten:

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

Außerdem, wenn Sie mount Komponenten benötigen , die zufällig withRouter oder useRouter und Sie sie nicht verspotten möchten, aber dennoch einige Tests gegen/um erstellen möchten Sie können diese HOC-Wrapper-Factory-Funktion zum Testen verwenden:

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

Anwendungsbeispiel:

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

Warum das verwenden? Weil es Ihnen ermöglicht, eine wiederverwendbare gemountete React-Komponente in einen Router-Kontext einzuschließen; und am wichtigsten ist, dass Sie wrapper.setProps(..) für die Root-Komponente aufrufen können!

Hallo, ich bekomme diesen Fehler:

TypeError: require.requireMock ist keine Funktion

DIESE LÖSUNG VERWENDET:

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");
War diese Seite hilfreich?
0 / 5 - 0 Bewertungen

Verwandte Themen

YarivGilad picture YarivGilad  ·  3Kommentare

irrigator picture irrigator  ·  3Kommentare

formula349 picture formula349  ·  3Kommentare

havefive picture havefive  ·  3Kommentare

rauchg picture rauchg  ·  3Kommentare