Next.js: Bagaimana cara mengejek useRouter?

Dibuat pada 1 Jun 2019  ·  21Komentar  ·  Sumber: vercel/next.js

Pertanyaan tentang Next.js

Saya ingin memastikan komponen saya dirender dengan benar dengan useRouter hook (sebenarnya saya mencoba memahami cara kerja perutean dinamis baru), jadi saya punya kode:

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;

Dan yang saya coba adalah:

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

Tetapi saya mendapatkan kesalahan TypeError: Cannot read property 'query' of null yang menunjuk pada baris const router = useRouter(); .

PS Saya tahu perutean dinamis tersedia di versi canary hanya untuk saat ini dan mungkin berubah, tetapi saya mendapatkan masalah dengan router, bukan dengan fitur WIP (apakah saya?).

Komentar yang paling membantu

Saya akhirnya mengejeknya seperti ini, saya hanya perlu ekspor useRouter jadi ini bekerja cukup baik untuk tujuan saya:

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

Semua 21 komentar

Hai, fitur ini masih eksperimental tetapi useRouter menggunakan React.useContext untuk menggunakan konteks dari next-server/dist/lib/router-context . Untuk mengejeknya, Anda harus membungkusnya dalam penyedia konteks dari sana yang mirip dengan baris ini

@ijjk Hai, terima kasih!
Saya tidak tahu apakah saya melakukannya dengan benar, tetapi tes lulus 😂

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

Jika ada cara yang lebih abstrak untuk mengejek params kueri, jadi saya dapat melewati rute aktual ( /users/nikita misalnya) dan meneruskan jalur ke file? Bagaimana menurutmu?

Mungkin lebih baik untuk mengejek router secara langsung daripada memanggil createRouter karena API itu internal dan dapat berubah kapan saja. Berikut ini contohnya:

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 itu masuk akal. Terima kasih banyak!

Apakah ada cara untuk mengejek useRouter menggunakan Enzyme+Jest? Saya telah mencari secara online sebentar dan satu-satunya hasil relevan yang muncul adalah masalah ini.

Saya berhasil mengejeknya dengan cara ini.

import * as nextRouter from 'next/router';

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

jest.spyOn bekerja untuk saya juga -

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

Saya akhirnya mengejeknya seperti ini, saya hanya perlu ekspor useRouter jadi ini bekerja cukup baik untuk tujuan saya:

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

Jika ada orang di sini yang ingin mengejek useRouter hanya untuk menghindari gangguan dari perintah prefetch , maka tiruan sederhana yang mati ini akan berfungsi

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

contoh kasus penggunaan akan menjadi komponen formulir yang mencakup sesuatu seperti:

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

@ijjk Apakah perilaku itu berubah di versi terbaru? Saya harus mengimpor dari next/dist/next-server/lib/router-context . Itu tidak akan mengenali konteksnya jika saya menginstal next-server secara terpisah.

Saya memiliki masalah yang sama persis.
Kami berada di bawah 9. Tidak ada solusi yang menggunakan RouterContext.Provider benar-benar berfungsi.
Satu-satunya cara lulus tes saya adalah menggunakan solusi @aeksco sebagai objek global di atas tes. Jika tidak, useRouter selalu tidak terdefinisi.
Ini tidak ideal karena saya tidak dapat mengatur parameter yang berbeda untuk pengujian saya.
Ada ide tentang ini?

EDIT
Saya membuatnya bekerja dengan tiruan global dari impor next/router dan spyOn pada tiruan, yang memungkinkan saya untuk memanggil mockImplementation(() => ({// whatever you want}) di setiap pengujian.
Ini terlihat seperti:

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

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

Kemudian dalam tes:

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

Ini tidak ideal tapi setidaknya berhasil untuk saya

FWIW inilah yang saya tetapkan:

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

Saya membutuhkan sesuatu yang berfungsi baik di Storybook maupun di Jest. Ini sepertinya berhasil, Anda cukup mengatur <Routermock> suatu tempat di pohon komponen. Ini tidak ideal karena saya tidak suka mengesampingkan Router.router terus-menerus.

Saya pikir solusi mengejek resmi akan menyenangkan :)

Metode @smasontst bekerja untuk kami, tetapi hati-hati dengan mockImplementationOnce() ...jika komponen Anda perlu dirender lebih dari sekali selama pengujian, Anda akan mendapati bahwa komponen tersebut tidak menggunakan router tiruan Anda pada render kedua dan tes Anda akan gagal. Mungkin yang terbaik adalah selalu menggunakan mockImplementation() sebagai gantinya, kecuali jika Anda memiliki alasan khusus untuk menggunakan mockImplementationOnce() .

Saya harus merevisi implementasi awal saya karena saya membutuhkan status unik useRouter berdasarkan pengujian demi pengujian. Mengambil halaman dari contoh yang disediakan oleh @ nterol24s dan memperbaruinya untuk bertindak sebagai fungsi utilitas yang dapat saya panggil dalam pengujian saya:

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

Saya sekarang dapat melakukan hal-hal seperti:

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

Perhatikan komentar oleh @mbrowne - Anda akan mengalami masalah yang sama dengan pendekatan ini, tetapi Anda dapat membagi contoh di atas menjadi fungsi mockNextUseRouter dan mockNextUseRouterOnce jika perlu.

Juga BESAR :+1: untuk solusi ejekan resmi @timneutkens

Bagi siapa saja yang menginginkan instance Router diejek secara global , Anda dapat menempatkan folder __mocks__ mana saja dan menargetkan paket next/router seperti:

__mocks__/next/router/index.js (harus mengikuti pola struktur folder ini!)

Contoh di bawah ini menargetkan Router.push dan 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");

Sekarang, di mana pun ada import Router from "next/router"; itu akan menjadi contoh tiruan. Anda juga dapat menambahkan fungsi mockImplementation pada mereka karena mereka akan diejek secara global.
Jika Anda ingin instance ini disetel ulang untuk setiap pengujian, maka di jest.json tambahkan properti clearMocks .

Untuk referensi, inilah struktur Router jika Anda ingin menargetkan ekspor tertentu:

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

Selain itu, jika Anda harus mount komponen yang kebetulan menggunakan withRouter atau useRouter dan Anda tidak ingin mengejeknya tetapi masih ingin membuat beberapa pengujian terhadap/sekitar mereka, maka Anda dapat menggunakan fungsi pabrik pembungkus HOC ini untuk pengujian:

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

Contoh penggunaan:

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

Mengapa menggunakan ini? Karena memungkinkan Anda untuk memiliki komponen React terpasang yang dapat digunakan kembali yang dibungkus dalam konteks Router; dan yang terpenting, ini memungkinkan Anda untuk memanggil wrapper.setProps(..) pada komponen root!

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

Tak satu pun dari solusi ini bekerja untuk saya. Alur kerja "benar" juga dijelaskan di sini di dokumen Jest: https://jestjs.io/docs/en/es6-class-mocks#spying -on-methods-of-our-class

Namun, saya dapat melihat tiruannya, tetapi tidak merekam panggilan ...

Inilah test-utils.tsx saya saat ini. Saya suka ini jauh lebih baik daripada menggunakan tiruan 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 terima kasih! Bekerja dengan baik!

Solusi @ flybayer berfungsi untuk saya, namun saya harus menentukan tipe pengembalian pada fungsi render

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

...

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

Bagi siapa saja yang menginginkan instance Router diejek secara global , Anda dapat menempatkan folder __mocks__ mana saja dan menargetkan paket next/router seperti:

__mocks__/next/router/index.js (harus mengikuti pola struktur folder ini!)

Contoh di bawah ini menargetkan Router.push dan 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");

Sekarang, di mana pun ada import Router from "next/router"; itu akan menjadi contoh tiruan. Anda juga dapat menambahkan fungsi mockImplementation pada mereka karena mereka akan diejek secara global.
Jika Anda ingin instance ini disetel ulang untuk setiap pengujian, maka di jest.json tambahkan properti clearMocks .

Untuk referensi, inilah struktur Router jika Anda ingin menargetkan ekspor tertentu:

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

Selain itu, jika Anda harus mount komponen yang kebetulan menggunakan withRouter atau useRouter dan Anda tidak ingin mengejeknya tetapi masih ingin membuat beberapa pengujian terhadap/sekitar mereka, maka Anda dapat menggunakan fungsi pabrik pembungkus HOC ini untuk pengujian:

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

Contoh penggunaan:

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

Mengapa menggunakan ini? Karena memungkinkan Anda untuk memiliki komponen React terpasang yang dapat digunakan kembali yang dibungkus dalam konteks Router; dan yang terpenting, ini memungkinkan Anda untuk memanggil wrapper.setProps(..) pada komponen root!

hai, saya mendapatkan kesalahan ini:

TypeError: require.requireMock bukan fungsi

MENGGUNAKAN SOLUSI INI:

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");
Apakah halaman ini membantu?
0 / 5 - 0 peringkat