React Testing Library побудована на принципі: чим більше тести схожі на те, як застосунок використовують, тим більше впевненості вони дають. Тест взаємодіє з компонентом як користувач - бачить текст і кнопки, натискає, вводить, - а не перевіряє стан, props чи внутрішні методи.
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('додає товар у кошик', async () => {
const user = userEvent.setup();
render(<Product product={{ id: 1, title: 'Чашка', price: 250 }} />);
await user.click(screen.getByRole('button', { name: 'Додати в кошик' }));
expect(screen.getByRole('status')).toHaveTextContent('У кошику: 1');
});
Пріоритет запитів (від кращого до гіршого):
getByRoleзname- роль доступності (кнопка, посилання, поле, заголовок) і доступна назва. Так елементи знаходять і користувачі зчитувачів екрана;getByLabelText- поля форми за підписом;getByPlaceholderText,getByText,getByDisplayValue;getByAltText,getByTitle;getByTestId- лише коли інших способів немає.
Чому роль, а не клас чи data-testid:
- тест не ламається від рефакторингу: змінили розмітку, класи Tailwind, структуру - тест проходить, поки кнопка лишається кнопкою з тим самим текстом;
- тест перевіряє доступність: якщо кнопку зробили
<div onClick>,getByRole('button')її не знайде - і це правильно, бо клавіатурою та зчитувачем екрана її теж не використати; getByLabelTextпадає, якщо поле не пов'язане з підписом, - ще одна безкоштовна перевірка.
Варіанти запитів:
getBy...- елемент має бути, інакше помилка;queryBy...- повертаєnull, для перевірки відсутності (expect(screen.queryByText('Помилка')).not.toBeInTheDocument());findBy...- чекає на появу (асинхронно);getAllBy...тощо - кілька елементів.
screen замість деструктуризації результату render - коротше й не треба оновлювати деструктуризацію.
Налагодження: screen.debug() друкує поточний DOM, а screen.logTestingPlaygroundURL() дає посилання, де можна підібрати найкращий запит для елемента.
Докладніше в документації: Testing Library: пріоритет запитів