Spec-Zone.ru › React 17

Рецепты тестирования

Общие шаблоны тестирования компонентов React.

Примечание:

Эта страница предполагает, что вы используете Jest в качестве исполнителя тестов. Если вы используете другой исполнители тестов, вам может потребоваться скорректировать API, но общая структура решения, скорее всего, останется той же. Подробнее о настройке тестовой среды см. на странице Тестовые среды.

На этой странице мы будем в основном использовать функциональные компоненты. Однако эти стратегии тестирования не зависят от деталей реализации и работают так же хорошо и для классовых компонентов.

  • Настройка/Разрушение
  • act()
  • Отображение
  • Получение данных
  • Имитация модулей
  • События
  • Таймеры
  • Тестирование снимков
  • Несколько рендереров
  • Не хватает чего-то?

Настройка/Разрушение

Для каждого теста мы обычно хотим отобразить наше дерево React в элемент DOM, присоединённый к document. Это важно, чтобы он мог получать события DOM. Когда тест завершается, мы хотим «очистить» и извлечь дерево из document.

Один из распространённых способов сделать это — использовать пару блоков beforeEach и afterEach, чтобы они всегда выполнялись и изолировали эффекты теста для себя:

import { unmountComponentAtNode } from "react-dom";

let container = null;
beforeEach(() => {
  // setup a DOM element as a render target
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // cleanup on exiting
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

Вы можете использовать другой шаблон, но помните, что нам нужно выполнить очистку даже если тест завершится неудачей. В противном случае тесты могут стать «текущими», и один тест может изменить поведение другого теста. Это затруднит отладку.

act()

При написании тестов пользовательского интерфейса такие задачи, как отображение, события пользователей или получение данных, можно рассматривать как «единицы» взаимодействия с пользовательским интерфейсом. react-dom/test-utils предоставляет помощник под названием act(), который гарантирует, что все обновления, связанные с этими «единицами», были обработаны и применены к DOM, прежде чем вы сделаете какие-либо утверждения:

act(() => {
  // render components
});
// make assertions

Это помогает сделать ваши тесты более похожими на то, что пользователи будут испытывать при использовании вашего приложения. Остальные примеры используют act() для обеспечения этих гарантий.

Вам может показаться, что использование act() напрямую немного излишне. Чтобы избежать некоторых шаблонов, можно использовать библиотеку, такую как React Testing Library, помощники которой обернуты с act().

Примечание:

Название act происходит от шаблона Arrange-Act-Assert.

Отображение

Часто вам может потребоваться проверить, правильно ли компонент отображается для заданных свойств. Рассмотрим простой компонент, который отображает сообщение на основе свойства:

// hello.js

import React from "react";

export default function Hello(props) {
  if (props.name) {
    return <h1>Hello, {props.name}!</h1>;
  } else {
    return <span>Hey, stranger</span>;
  }
}

Мы можем написать тест для этого компонента:

// hello.test.js

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";

import Hello from "./hello";

let container = null;
beforeEach(() => {
  // setup a DOM element as a render target
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // cleanup on exiting
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("renders with or without a name", () => {
  act(() => {
    render(<Hello />, container);
  });
  expect(container.textContent).toBe("Hey, stranger");

  act(() => {
    render(<Hello name="Jenny" />, container);
  });
  expect(container.textContent).toBe("Hello, Jenny!");

  act(() => {
    render(<Hello name="Margaret" />, container);
  });
  expect(container.textContent).toBe("Hello, Margaret!");
});

Получение данных

Вместо вызова реальных API во всех ваших тестах вы можете имитировать запросы с тестовыми данными. Моделирование получения данных с использованием «фиктивных» данных предотвращает нестабильные тесты из-за недоступного бэкенда и ускоряет их выполнение. Примечание: вы всё ещё можете запустить подмножество тестов с помощью фреймворка «конец-в-конец» (end-to-end), который показывает, работает ли всё приложение вместе.

// user.js

import React, { useState, useEffect } from "react";

export default function User(props) {
  const [user, setUser] = useState(null);

  async function fetchUserData(id) {
    const response = await fetch("/" + id);
    setUser(await response.json());
  }

  useEffect(() => {
    fetchUserData(props.id);
  }, [props.id]);

  if (!user) {
    return "loading...";
  }

  return (
    <details>
      <summary>{user.name}</summary>
      <strong>{user.age}</strong> years old
      <br />
      lives in {user.address}
    </details>
  );
}

Мы можем написать тесты для него:

// user.test.js

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import User from "./user";

let container = null;
beforeEach(() => {
  // setup a DOM element as a render target
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // cleanup on exiting
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("renders user data", async () => {
  const fakeUser = {
    name: "Joni Baez",
    age: "32",
    address: "123, Charming Avenue"
  };

  jest.spyOn(global, "fetch").mockImplementation(() =>
    Promise.resolve({
      json: () => Promise.resolve(fakeUser)
    })
  );

  // Use the asynchronous version of act to apply resolved promises
  await act(async () => {
    render(<User id="123" />, container);
  });

  expect(container.querySelector("summary").textContent).toBe(fakeUser.name);
  expect(container.querySelector("strong").textContent).toBe(fakeUser.age);
  expect(container.textContent).toContain(fakeUser.address);

  // remove the mock to ensure tests are completely isolated
  global.fetch.mockRestore();
});

Имитация модулей

Некоторые модули могут работать плохо в тестовой среде или могут не быть такими важными для самого теста. Имитация таких модулей с помощью заглушек может упростить написание тестов для вашего собственного кода.

Рассмотрим компонент Contact , который встраивает компонент третьей стороны GoogleMap:

// map.js

import React from "react";

import { LoadScript, GoogleMap } from "react-google-maps";
export default function Map(props) {
  return (
    <LoadScript id="script-loader" googleMapsApiKey="YOUR_API_KEY">
      <GoogleMap id="example-map" center={props.center} />
    </LoadScript>
  );
}

// contact.js

import React from "react";
import Map from "./map";

export default function Contact(props) {
  return (
    <div>
      <address>
        Contact {props.name} via{" "}
        <a data-testid="email" href={"mailto:" + props.email}>
          email
        </a>
        or on their <a data-testid="site" href={props.site}>
          website
        </a>.
      </address>
      <Map center={props.center} />
    </div>
  );
}

Если мы не хотим загружать этот компонент в наши тесты, мы можем смоделировать зависимость самой заглушкой-компонентом и выполнить наши тесты:

// contact.test.js

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";

import Contact from "./contact";
import MockedMap from "./map";

jest.mock("./map", () => {
  return function DummyMap(props) {
    return (
      <div data-testid="map">
        {props.center.lat}:{props.center.long}
      </div>
    );
  };
});

let container = null;
beforeEach(() => {
  // setup a DOM element as a render target
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // cleanup on exiting
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("should render contact information", () => {
  const center = { lat: 0, long: 0 };
  act(() => {
    render(
      <Contact
        name="Joni Baez"
        email="test@example.com"
        site="http://test.com"
        center={center}
      />,
      container
    );
  });

  expect(
    container.querySelector("[data-testid='email']").getAttribute("href")
  ).toEqual("mailto:test@example.com");

  expect(
    container.querySelector('[data-testid="site"]').getAttribute("href")
  ).toEqual("http://test.com");

  expect(container.querySelector('[data-testid="map"]').textContent).toEqual(
    "0:0"
  );
});

События

Мы рекомендуем отправлять реальные события DOM на элементы DOM, а затем утверждать результат. Рассмотрим компонент Toggle:

// toggle.js

import React, { useState } from "react";

export default function Toggle(props) {
  const [state, setState] = useState(false);
  return (
    <button
      onClick={() => {
        setState(previousState => !previousState);
        props.onChange(!state);
      }}
      data-testid="toggle"
    >
      {state === true ? "Turn off" : "Turn on"}
    </button>
  );
}

Мы могли бы написать тесты для него:

// toggle.test.js

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";

import Toggle from "./toggle";

let container = null;
beforeEach(() => {
  // setup a DOM element as a render target
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // cleanup on exiting
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("changes value when clicked", () => {
  const onChange = jest.fn();
  act(() => {
    render(<Toggle onChange={onChange} />, container);
  });

  // get a hold of the button element, and trigger some clicks on it
  const button = document.querySelector("[data-testid=toggle]");
  expect(button.innerHTML).toBe("Turn on");

  act(() => {
    button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
  });

  expect(onChange).toHaveBeenCalledTimes(1);
  expect(button.innerHTML).toBe("Turn off");

  act(() => {
    for (let i = 0; i < 5; i++) {
      button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
    }
  });

  expect(onChange).toHaveBeenCalledTimes(6);
  expect(button.innerHTML).toBe("Turn on");
});

Различные события DOM и их свойства описаны на сайте MDN. Обратите внимание, что вам нужно передать { bubbles: true } в каждое создаваемое вами событие, чтобы оно достигло слушателя React, так как React автоматически делегирует события в корень.

Примечание:

React Testing Library предлагает более краткий помощник для запуска событий.

Таймеры

Ваш код может использовать функции, основанные на таймерах, такие как setTimeout , чтобы запланировать больше работы в будущем. В этом примере панель с множественным выбором ожидает выбора и переходит к следующему этапу, отключаясь, если выбор не сделан в течение 5 секунд:

// card.js

import React, { useEffect } from "react";

export default function Card(props) {
  useEffect(() => {
    const timeoutID = setTimeout(() => {
      props.onSelect(null);
    }, 5000);
    return () => {
      clearTimeout(timeoutID);
    };
  }, [props.onSelect]);

  return [1, 2, 3, 4].map(choice => (
    <button
      key={choice}
      data-testid={choice}
      onClick={() => props.onSelect(choice)}
    >
      {choice}
    </button>
  ));
}

Мы можем написать тесты для этого компонента, используя модели таймеров Jest и тестирование различных состояний, в которых он может находиться.

// card.test.js

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";

import Card from "./card";

let container = null;
beforeEach(() => {
  // setup a DOM element as a render target
  container = document.createElement("div");
  document.body.appendChild(container);
  jest.useFakeTimers();
});

afterEach(() => {
  // cleanup on exiting
  unmountComponentAtNode(container);
  container.remove();
  container = null;
  jest.useRealTimers();
});

it("should select null after timing out", () => {
  const onSelect = jest.fn();
  act(() => {
    render(<Card onSelect={onSelect} />, container);
  });

  // move ahead in time by 100ms
  act(() => {
    jest.advanceTimersByTime(100);
  });
  expect(onSelect).not.toHaveBeenCalled();

  // and then move ahead by 5 seconds
  act(() => {
    jest.advanceTimersByTime(5000);
  });
  expect(onSelect).toHaveBeenCalledWith(null);
});

it("should cleanup on being removed", () => {
  const onSelect = jest.fn();
  act(() => {
    render(<Card onSelect={onSelect} />, container);
  });

  act(() => {
    jest.advanceTimersByTime(100);
  });
  expect(onSelect).not.toHaveBeenCalled();

  // unmount the app
  act(() => {
    render(null, container);
  });

  act(() => {
    jest.advanceTimersByTime(5000);
  });
  expect(onSelect).not.toHaveBeenCalled();
});

it("should accept selections", () => {
  const onSelect = jest.fn();
  act(() => {
    render(<Card onSelect={onSelect} />, container);
  });

  act(() => {
    container
      .querySelector("[data-testid='2']")
      .dispatchEvent(new MouseEvent("click", { bubbles: true }));
  });

  expect(onSelect).toHaveBeenCalledWith(2);
});

Вы можете использовать фиктивные таймеры только в некоторых тестах. Выше мы включили их, вызвав jest.useFakeTimers(). Основное преимущество, которое они предоставляют, заключается в том, что вашему тесту фактически не нужно ждать пять секунд для выполнения, и вам также не нужно усложнять код компонента только для тестирования.

Тестирование снимков

Фреймворки, такие как Jest, также позволяют сохранять «снимки» данных с помощью toMatchSnapshot / toMatchInlineSnapshot. С их помощью мы можем «сохранить» вывод отображаемого компонента и убедиться, что изменение его требует явного внесения изменений в снимок.

В этом примере мы отображаем компонент и форматируем рендер HTML с помощью пакета pretty, прежде чем сохранить его как встроенный снимок:

// hello.test.js, again

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import pretty from "pretty";

import Hello from "./hello";

let container = null;
beforeEach(() => {
  // setup a DOM element as a render target
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // cleanup on exiting
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("should render a greeting", () => {
  act(() => {
    render(<Hello />, container);
  });

  expect(
    pretty(container.innerHTML)
  ).toMatchInlineSnapshot(); /* ... gets filled automatically by jest ... */

  act(() => {
    render(<Hello name="Jenny" />, container);
  });

  expect(
    pretty(container.innerHTML)
  ).toMatchInlineSnapshot(); /* ... gets filled automatically by jest ... */

  act(() => {
    render(<Hello name="Margaret" />, container);
  });

  expect(
    pretty(container.innerHTML)
  ).toMatchInlineSnapshot(); /* ... gets filled automatically by jest ... */
});

Обычно лучше делать более конкретные утверждения, чем использовать снимки. Эти виды тестов включают детали реализации, поэтому они легко ломаются, и команды могут потерять чувствительность к сбоям снимков. Выборочная имитация некоторых дочерних компонентов может помочь уменьшить размер снимков и сохранить их удобочитаемыми для анализа кода.

Несколько рендереров

В редких случаях вы можете запускать тест на компоненте, использующем несколько рендереров. Например, вы можете запускать тесты снимков на компоненте с react-test-renderer, который вложенно использует ReactDOM.render внутри дочернего компонента для отображения содержимого. В этом случае вы можете обернуть обновления с act() , соответствующими их рендерерам.

import { act as domAct } from "react-dom/test-utils";
import { act as testAct, create } from "react-test-renderer";
// ...
let root;
domAct(() => {
  testAct(() => {
    root = create(<App />);
  });
});
expect(root).toMatchSnapshot();

Не хватает чего-то?

Если какой-либо распространённый сценарий не охвачен, сообщите нам об этом на странице отслеживания проблем веб-сайта документации.

Полезная ли эта страница?

© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://17.reactjs.org/docs/testing-recipes.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API