Как протестировать логику маршрутизации с помощью React Router v6 и тестовой библиотеки?

Я перешел с React Router v5 на v6, следуя этому туториалу . Я хочу протестировать его с помощью react-testing-library , но мои старые модульные тесты (с использованием шаблона в этом документе) перестали работать.

Мое приложение с React Router v6 выглядит так

const router = createBrowserRouter([
    {
        path: "/",
        element: (
            <>
                <SiteHeader />
                <Outlet />
            </>
        ),
        errorElement: <NotFound />,
        children: [
            { path: "/", element: <Home /> },
            { path: "/posts", element: <Posts /> },
            { path: "/post/:postId", element: <PostPage /> },
        ],
    },
]);

function App() {
    return (
        <div className = "app">
            <RouterProvider router = {router} />
        </div>
    );
}

Как видите, он использует RouterProvider вместо Switch/Route (поэтому я сбит с толку тем, что в этом ТАК вопросе говорится, что он использует React Router v6, но он выглядит совсем по-другому.).

Код в официальном документе библиотеки тестирования тоже не использует RouterProvider.

Я хочу протестировать некоторую логику маршрутизации, подобную этому псевдокоду:

renderWithRouter(<App />, "/posts"); // loads /posts page initially
await user.click(screen.getByText("some post title")); // trigger click
expect(getUrl(location)).toEqual("/post/123"); // checks the URL changed correctly

Как я могу создать функцию renderWithRouter, такую ​​​​как this с RouterProvider? Обратите внимание, что этот renderWithRouter работал у меня, когда я использовал React Router v5, но после перехода на v6 он перестал работать.

Мои текущие версии зависимостей:

  • "реагировать": "^ 18.2.0",
  • "реагировать-дом": "^ 18.2.0",
  • "реакция-маршрутизатор-дом": "^ 6.4.3",
  • "@testing-library/jest-dom": "^5.16.5",
  • "@testing-library/реагировать": "^13.4.0",
  • "@testing-library/user-event": "^14.4.3",

я пробовал это

test("click post goes to /post/:postId", async () => {
    render(
        <MemoryRouter initialEntries = {["/posts"]}>
            <App />
        </MemoryRouter>,
    );
    // ...
});

но я получил ошибку

You cannot render a <Router> inside another <Router>. You should never have more than one in your app.

      31 | test("click post goes to /post/:postId", async () => {
    > 32 |     render(
         |     ^
      34 |         <MemoryRouter initialEntries = {["/posts"]}>
      36 |             <App />

Что вы пытаетесь протестировать? Вы должны тестировать блоки своего кода, а не стороннего кода. Что может помешать вам обернуть компонент Posts в MemoryRouter и протестировать Posts поведение, например?

Drew Reese 11.11.2022 08:57

@DrewReese Обновлено. Пожалуйста, смотрите часть псевдокода для того, что я хочу протестировать. Я получил сообщение об ошибке при использовании MemoryRouter (см. конец)

lzl124631x 11.11.2022 09:10

Я не думаю, что вы правильно думаете о «единице», как при тестировании наименьшей необходимой единицы кода. Если вы пытаетесь протестировать компонент Post, попробуйте рендерить только Post. Опять же, это зависит от того, что вы пытаетесь протестировать. Иногда некоторые компоненты необходимо отображать в контексте маршрутизации, поэтому для предоставления контекста необходим маршрутизатор.

Drew Reese 11.11.2022 09:13

FWIW тест для перехода с одной страницы на другую не является модульным тестом, он больше граничит с интеграционным тестированием (то есть, как две или более единицы кода интегрируются вместе). react-testing-library не подходит для работы по интеграционному тестированию. Для этого ищите что-то вроде puppeteer, selenium, cypress и т. д.

Drew Reese 11.11.2022 09:16
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
1
5
994
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Если вы хотите протестировать конфигурацию ваших маршрутов в целом, используя новые [email protected] маршрутизаторы данных, я бы предложил немного рефакторить код, чтобы можно было заглушить MemoryRouter для любого модульного тестирования.

Объявить конфигурацию маршрутов самостоятельно и экспортировать.

const routesConfig = [
  {
    path: "/",
    element: (
      <>
        <SiteHeader />
        <Outlet />
      </>
    ),
    errorElement: <NotFound />,
    children: [
      { path: "/", element: <Home /> },
      { path: "/posts", element: <Posts /> },
      { path: "/post/:postId", element: <PostPage /> },
    ],
  },
];

export default routesConfig;

В коде приложения импортируйте routesConfig и создайте экземпляр BrowserRouter, который использует приложение.

import {
  RouterProvider,
  createBrowserRouter,
} from "react-router-dom";
import routesConfig from '../routes';

const router = createBrowserRouter(routesConfig);

function App() {
  return (
    <div className = "app">
      <RouterProvider router = {router} />
    </div>
  );
}

Для модульных тестов импортируйте routesConfig и создайте экземпляр MemoryRouter.

import {
  RouterProvider,
  createMemoryRouter,
} from "react-router-dom";
import { render, waitFor } from "@testing-library/react";
import routesConfig from '../routes';

...

test("click post goes to /post/:postId", async () => {
  const router = createMemoryRouter(routesConfig, {
    initialEntries: ["/posts"],
  });

  render(<RouterProvider router = {router} />);

  // make assertions, await changes, etc...
});

Потрясающие. Спасибо! Это работает! С MemoryRouter мне нужно использовать router.state.location, когда я хочу проверить текущий путь или выполнить поиск.

lzl124631x 11.11.2022 10:11

Кстати, testing-library.com/docs/example-react-router, вероятно, следует обновить это. :D

lzl124631x 11.11.2022 10:15

FWIW, я создал свой собственный renderWithRouter для React Router V6.

export const renderWithRouter = (route = "/") => {
    window.history.pushState({}, "Test page", route);
    return {
        user: userEvent.setup(),
        ...render(<RouterProvider router = {createBrowserRouter(routes)} />),
    };
};

А это примерный тест.

test("click Posts => shows Posts page", async () => {
    const { user } = renderWithRouter();
    const postsLink = screen.getByText("Posts").closest("a");
    expect(postsLink).not.toHaveClass("active");
    await user.click(postsLink as HTMLAnchorElement);
    expect(postsLink).toHaveClass("active");
    expect(getUrl(location)).toEqual("/posts");
});

Другие вопросы по теме