Главная Категории Контакты Поиск

React Testing Library

Распространенные ошибки при использовании React Testing Library.

Testing LibraryReact·27.08.2020·читать 5 мин 🤓·Автор: Alexey Myzgin

Использование имени wrapper в качестве имени переменной для возвращаемого значения от render

// ❌
const wrapper = render(<Example prop="1" />);
wrapper.rerender(<Example prop="2" />);
// ✅
const { rerender } = render(<Example prop="1" />);
rerender(<Example prop="2" />);

Имя wrapper - это старая фраза от enzyme, и нам она здесь не нужно. Возвращаемое значение от render ничего не «оборачивает». Это просто набор утилит.

Использование cleanup

// ❌
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
afterEach(cleanup);

// ✅
import { render, screen, fireEvent } from "@testing-library/react";

cleanup происходит автоматически (поддерживается большинством основных сред тестирования). Так что тебе больше не нужно об этом беспокоиться. Более подробнее здесь.

Используй screen

// ❌
const { getByRole } = render(<Example />);
const errorMessageNode = getByRole("alert");

// ✅
render(<Example />);
const errorMessageNode = screen.getByRole("alert");

screen был добавлен в DOM Testing Library v6.11.0 (это означает, что ты должен иметь к нему доступ в @testing-library/react@9). Он исходит из того же оператора импорта, из которого ты получаешь render:

import { render, screen } from "@testing-library/react";

Преимущество использования screen состоит в том, что тебе больше не нужно обновлять деструктуризацию render по мере добавления / удаления необходимых запросов. Тебе нужно только набрать screen. и пусть волшебное автозаполнение твоего редактора позаботится обо всем остальном.

Единственное исключение из этого - если ты устанавливаешь container или baseElement, чего тебе, вероятно, следует избегать (они существуют только по историческим причинам на данный момент).

Ты также можешь вызвать screen.debug вместо debug

Использование неправильных утверждений

const button = screen.getByRole("button", { name: /disabled button/i });

// ❌
expect(button.disabled).toBe(true);
// error message:
//  expect(received).toBe(expected) // Object.is equality
//
//  Expected: true
//  Received: false

// ✅
expect(button).toBeDisabled();
// error message:
//   Received element is not disabled:
//     <button />

Утверждение toBeDisabled исходит от jest-dom. Настоятельно рекомендуется использовать jest-dom, так как сообщения об ошибках, которые ты получаешь с ним намного лучше.

Совет: установи и используй @testing-library/jest-dom

Использование неправильных запросов

// ❌
// предполагая, что у тебя есть этот DOM для работы:
// <label>Username</label><input data-testid="username" />
screen.getByTestId("username");

// ✅
// измени DOM, чтобы он был доступен, связав label и установив type
// <label for="username">Username</label><input id="username" type="text" />
screen.getByRole("textbox", { name: /username/i });

Есть страница под названием “Какой запрос я должен использовать?” состоящая из запросов, которые ты должен попытаться использовать в определённом порядке.

Поиск по тексту

// ❌
screen.getByTestId("submit-button");

// ✅
screen.getByRole("button", { name: /submit/i });

Если ты не делаешь запрос по фактическому тексту, то нужно проделать дополнительную работу, чтобы убедиться, что твои текст применяется правильно.

Используй ByRole большую часть времени

Опция name позволяет запрашивать элементы по их «Доступному имени», и это работает, даже если элемент имеет текстовое содержимое, разделенное на разные элементы. Например:

// например у нас есть такая структура DOM
// <button><span>Hello</span> <span>World</span></button>

screen.getByText(/hello world/i);
// ❌ ошибка:
// Unable to find an element with the text: /hello world/i. This could be
// because the text is broken up by multiple elements. In this case, you can
// provide a function for your text matcher to make your matcher more flexible.

screen.getByRole("button", { name: /hello world/i });
// ✅ работает!

Одна из причин, по которой люди не используют запросы ByRole, заключается в том, что они не знакомы с неявными ролями, размещаемыми в элементах. Вот список ролей на MDN. Еще одна особенность запросов ByRole - это то, что если он не может найти элемент с указанной ролью, то не только выводится весь DOM в log, но также выводятся все доступные роли, по которым ты можешь сделать запрос.

// например у нас есть такая структура DOM
// <button><span>Hello</span> <span>World</span></button>
screen.getByRole("test");
TestingLibraryElementError: Unable to find an accessible element with the role "test"

Here are the accessible roles:

  button:
  Name "Hello World":
  <button />

  --------------------------------------------------

<body>
  <div>
    <button>
      <span>
        Hello
      </span>
      <span>
        World
      </span>
    </button>
  </div>
</body>

Обрати внимание, что не нужно было добавлять role=button к кнопке, чтобы она имела role=button.

Совет. Прочти и следуй рекомендациям руководства “Which query should I use?”.

Неправильное добавление атрибутов aria-, role и т.д.

// ❌
render(<button role="button">Click me</button>);

// ✅
render(<button>Click me</button>);

Добавление атрибутов не только не нужно (как в случае выше), но это может запутать программы чтения с экрана и пользователей. Атрибуты специальных возможностей следует использовать только тогда, когда семантический HTML не удовлетворяет твой вариант использования. Следуй рекомендациям WAI-ARIA; у них есть отличные примеры.

Примечание: чтобы сделать input доступными через “role” - тебе нужно указать атрибут type!

Используй @testing-library/user-event

// ❌
fireEvent.change(input, { target: { value: "hello world" } });

// ✅
userEvent.type(input, "hello world");

@testing-library/user-event - это пакет, созданный поверх fireEvent, но он предоставляет несколько методов, которые более похожи на взаимодействие с пользователем. В приведенном выше примере fireEvent.change просто вызовет одно событие change на input. Однако, type будет вызывать события keyDown, keyPress и keyUp для каждого символа. Это намного ближе к фактическим взаимодействиям пользователя. Это дает преимущество работы с библиотеками, которые не слушают change события.

Используй query* для поиска чего угодно, кроме проверки на “несуществование”

// ❌
expect(screen.queryByRole("alert")).toBeInTheDocument();

// ✅
expect(screen.getByRole("alert")).toBeInTheDocument();
expect(screen.queryByRole("alert")).not.toBeInTheDocument();

Единственная причина, по которой стоит использовать query*, заключается в том, что он не выдает ошибку, если не найдено ни одного элемента, по соответствующему запросу (он возвращает ноль, если элемент не найден). Единственная причина, по которой это полезно - убедиться, что элемент не отображается на странице.

Использование waitFor для ожидания элементов, которые могут быть запрошены с помощью find*

// ❌
const submitButton = await waitFor(() =>
  screen.getByRole("button", { name: /submit/i }),
);

// ✅
const submitButton = await screen.findByRole("button", { name: /submit/i });

Эти два примера кода эквивалентны (запросы find* используют waitFor под капотом), но второй вариант проще, и полученное сообщение об ошибке будет понятнее.

Совет: используй find* в любое время, когда нужно запросить что-то, что может быть недоступно сразу.

Передача пустого обратного вызова в waitFor

// ❌
await waitFor(() => {});
expect(window.fetch).toHaveBeenCalledWith("foo");
expect(window.fetch).toHaveBeenCalledTimes(1);

// ✅
await waitFor(() => expect(window.fetch).toHaveBeenCalledWith("foo"));
expect(window.fetch).toHaveBeenCalledTimes(1);

Цель waitFor состоит в том, чтобы позволить нам ждать, пока что-то конкретное произойдет. Если ты передашь пустой обратный вызов, он может сработать, потому что всё, что придется ждать - «один круг цикла событий». Но у тебя останется хрупкий тест, который может легко “упасть”, если ты поменяешь свою асинхронную логику.

Выполнение побочных эффектов в waitFor

// ❌
await waitFor(() => {
  fireEvent.keyDown(input, { key: "ArrowDown" });
  expect(screen.getAllByRole("listitem")).toHaveLength(3);
});
// ✅
fireEvent.keyDown(input, { key: "ArrowDown" });
await waitFor(() => {
  expect(screen.getAllByRole("listitem")).toHaveLength(3);
});

waitFor предназначен для вещей, которые имеют недетерминированный промежуток времени между выполненным тобой действием и передачей утверждения. Из-за этого обратный вызов может вызываться (или проверяться на наличие ошибок) недетерминированным количество раз (он вызывается как на интервале, так и при наличии мутаций DOM). Так что это означает, что твой побочный эффект может запускаться несколько раз!

Это также означает, что ты не можешь использовать snapshot утверждения в waitFor. Если хочешь использовать snapshot утверждение, то сначала дождись определенного подтверждения, а затем, после этого, можешь сделать свой snapshot.

Используй плагин ESLint для библиотеки тестирования

Если ты хочешь избежать некоторых распространенных ошибок, то официальные плагины ESLint могут сильно тебе в этом помочь:

Подробнее о ESLint здесь

Website, name & logo
Copyright © 2022. Alex Myzgin