- Использование имени
wrapper
- Использование
cleanup
- Используй
screen
- Использование неправильных утверждений
- Использование неправильных запросов
- Поиск по тексту
- Используй
ByRole
- Неправильное добавление атрибутов
- Используй @testing-library/user-event
- Используй
query*
- Использование
waitFor
для ожидания элементов - Передача пустого обратного вызова в
waitFor
- Выполнение побочных эффектов в
waitFor
- Используй плагин
ESLint
для библиотеки тестирования
Использование имени 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 здесь