React
Тестирование: RTL и Playwright
Тест проверяет, что у компонента в состоянии лежит count === 1, лезет в приватный метод и сверяет имя CSS-класса. Через неделю разработчик переименовывает переменную состояния и меняет вёрстку - функциональность та же, но десять тестов краснеют. Тесты, привязанные к реализации, ломаются от рефакторинга и ничего не говорят о том, работает ли продукт для пользователя. React Testing Library перевернула подход: проверяй то, что видит и делает пользователь, а не то, как устроены внутренности. Тогда рефакторинг проходит молча, а падение теста означает реальную регрессию.
- Компонентные тесты дизайн-систем: кнопка, модалка, форма проверяются по доступной роли и видимому тексту
- E2E-сценарии на Playwright: вход, оформление заказа, оплата прогоняются в реальном браузере
- Асинхронные интерфейсы с загрузкой и Suspense, где тест ждёт появления данных, а не таймера
- MSW мокирует сетевой слой, поэтому тесты не ходят в настоящий backend и стабильны
- CI-пайплайны, где быстрые тесты RTL и медленные e2e Playwright дополняют друг друга
Предварительные знания
- Модель рендера: компонент отображает UI из состояния и пропсов
- Состояние и обработчики событий, обновление интерфейса по действию
- Базовое понимание промисов и асинхронной загрузки данных
Сдвиг от тестов реализации к тестам поведения
Ранние инструменты вроде Enzyme поощряли заглядывать внутрь компонента: проверять состояние, вызывать методы, искать узлы по именам классов. Такие тесты ломались от любого рефакторинга, даже когда поведение не менялось. В 2018 Кент Доддс выпустил React Testing Library с принципом: чем больше тест похож на то, как продукт используют, тем больше он внушает доверия. Запросы строятся по доступной роли и тексту, как у реального пользователя и скринридера. Параллельно Microsoft развивала Playwright (2020) для e2e в настоящем браузере, а Mock Service Worker дал перехват сети на уровне сервис-воркера.
Поведение вместо реализации: роли и user-event
Главный принцип RTL: тест должен взаимодействовать с компонентом так же, как пользователь, и проверять то, что пользователь видит. Не внутреннее состояние и не имена классов, а отрисованный результат. Поэтому элементы ищут по доступной роли (кнопка, заголовок, поле ввода) и по видимому тексту, а не по селекторам, привязанным к деталям вёрстки.
user-event имитирует поведение пользователя ближе к реальности, чем низкоуровневый fireEvent. Клик через user-event это последовательность реальных событий: наведение, нажатие, отпускание, фокус. Ввод текста печатает посимвольно, запуская обработчики на каждом символе. Это ловит баги, которые единичный fireEvent.change пропустил бы.
| Запрос | Когда вернёт | Применение |
|---|---|---|
| getBy | Сразу, бросает ошибку если нет | Элемент точно есть на экране |
| queryBy | Сразу, null если нет | Проверка отсутствия элемента |
| findBy | Промис, ждёт появления | Асинхронный элемент после загрузки |
Поиск по тестовому идентификатору data-testid это запасной вариант, а не основной. Приоритет за ролью и текстом: такие запросы заодно проверяют доступность интерфейса. Если элемент не находится по роли, часто это сигнал, что разметка недоступна для скринридеров.
Почему RTL советует искать элементы по роли и тексту, а не по именам CSS-классов или состоянию компонента?
Асинхронность, Suspense и мокирование сети через MSW
Асинхронный интерфейс сначала показывает загрузку, потом данные. Тест не должен ждать фиксированную задержку - это хрупко. Вместо этого используют findBy или waitFor: они опрашивают DOM, пока нужный элемент не появится, с разумным таймаутом. То же касается Suspense: тест проверяет сначала фолбэк, затем дождавшись данных - результат.
Чтобы тест не ходил в настоящий backend, сеть мокируют. Mock Service Worker перехватывает запросы на уровне сетевого слоя по тем же URL, что и в проде. Компонент делает обычный fetch и не знает, что ответ пришёл от мока. Один набор обработчиков MSW переиспользуется и в тестах, и в storybook, и в локальной разработке.
MSW позволяет переопределить обработчик в отдельном тесте через server.use, чтобы вернуть ошибку 500 или пустой список. Так проверяются ветки загрузки, ошибки и пустого состояния без изменения компонента - меняется только ответ сети.
Как правильно дождаться асинхронных данных в тесте RTL?
Playwright: сквозные сценарии в реальном браузере
RTL проверяет компоненты в смоделированном окружении быстро, но не запускает реальный браузер целиком. Сквозные сценарии - вход, оформление заказа, оплата - проверяет Playwright: он управляет настоящим Chromium, Firefox и WebKit, кликает, печатает и ждёт так же, как пользователь. Это другой уровень пирамиды тестов: их меньше, они медленнее, но покрывают всю цепочку от UI до backend.
Playwright построен на авто-ожидании: каждое действие и проверка сами ждут, пока элемент станет видимым и готовым, без ручных задержек. Локаторы здесь, как и в RTL, ориентированы на роль и доступный текст, поэтому стиль запросов узнаваем. Это снижает нестабильность, главную болезнь e2e-тестов прошлого.
- React Testing Library — Компонентные и интеграционные тесты в смоделированном DOM. Быстрые, их много. Проверяют поведение отдельных частей UI.
- Playwright — Сквозные тесты в настоящем браузере через всю систему. Медленнее, их меньше. Проверяют критичные пользовательские сценарии целиком.
Здоровая стратегия это сочетание уровней: много быстрых тестов RTL на отдельные компоненты и поведение, и небольшой набор e2e Playwright на ключевые сценарии. Дублировать всю функциональность в медленных e2e нецелесообразно - они дороги в поддержке и времени.
Как соотносятся роли React Testing Library и Playwright в стратегии тестирования?
Связь с другими темами
Этот урок про тестирование. Рядом стоят соседние темы:
- Модель рендера — Тесты проверяют наблюдаемый результат рендера из состояния, а не внутреннюю механику
- Формы с RHF и Zod — Формы это частый объект тестирования: ввод через user-event и проверка сообщений валидации
Итог
- React Testing Library проверяет наблюдаемое поведение, а не реализацию, поэтому рефакторинг не ломает тесты без причины
- Запросы строятся по доступной роли и видимому тексту (getByRole, getByText), как взаимодействует реальный пользователь
- user-event имитирует действия пользователя ближе к реальности, чем низкоуровневый fireEvent
- Асинхронность и Suspense тестируются ожиданием появления элемента (findBy, waitFor), а не фиксированными задержками
- MSW мокирует сеть на уровне перехвата запросов, так что тесты не зависят от настоящего backend и стабильны
- Playwright прогоняет e2e в реальном браузере: RTL покрывает компоненты быстро, Playwright проверяет сквозные сценарии
Связанные уроки
- rc-10-render-mental-model — Понимание того, что компонент рендерит из состояния, лежит в основе проверки наблюдаемого поведения
- rc-39-forms-zod — Формы это типичный объект тестирования: ввод через user-event и проверка сообщений валидации