Frontend Meetup от OneTwoTrip: создаём надёжные тесты с Playwright

Продолжаем рассказывать подробности про наш офлайн-митап для фронтендеров, который прошёл в Москве 20 марта 2025. В этой серии — презентация Никиты Велько. Он рассказал, как добиться стабильности, высокой скорости и предсказуемости тестирования с Playwright. А ещё поделился хитростями настройки и рассмотрел типичные ловушки, с которыми сталкиваются команды.

Ссылка на выступление: https://vkvideo.ru/video-229335646_456239087

О спикере

Frontend Meetup от OneTwoTrip: создаём надёжные тесты с Playwright

Никита Велько — Team Lead в команде «Маркетинг». Фронтенд-разработкой занимается уже 10 лет, из них полтора года работает в OneTwoTrip. Его команда делает спецпроекты для увеличения пользовательской базы сервиса — в основном это игры, например, «Раз, два, едем» и «Азия на миллион». Все они отличаются богатым визуалом и большим количеством интерактивных элементов. Естественно, всё это надо покрыть тестами, чтобы быть уверенными, что проекты правильно работают в разных браузерах.

В чём проблема?

Сразу договоримся, что под тестами в данном случае понимается то, что одни называют UI-тестами, а другие — end-to-end (E2E) тестами.

Что такое тест в базовом понимании? Мы открываем наше приложение, при необходимости замокали какие-то эндпоинты, провели различные манипуляции со страницей (нажали на кнопки, заполнили поля) — и таким образом проверили, что поведение приложения совпадает с нашими ожиданиями.

Для такой задачи Playwright — отличный инструмент. С ним легко начать писать первые тесты. Но если не уделять внимание тому, как именно вы пишете эти тесты, очень скоро начнутся проблемы. Тесты будут хрупкими, пайплайн начнёт через раз падать, а вам придётся постоянно жать на кнопку Rebuild, чтобы просто дождаться зелёного пайплайна и передать задачу дальше.

Чтобы такой ситуации не было, давайте разбираться, как создавать надёжные тесты и как повысить надёжность тех, что уже написаны.

Что такое надёжный тест

Можно выделить несколько критериев:

  • Стабильный: неважно, сколько раз мы его запустили, если входные условия не изменились, мы всегда получим один и тот же результат.
  • Предсказуемый: неважен порядок запусков тестов, на какой машине мы это делаем, в каком часовом поясе и другие внешние факторы.
  • Легкочитаемый: мы можем быстро погрузиться в детали реализации.
  • Выявляет проблемы: если тест упал, значит, сломался функционал, и нужно его чинить.

Почему вообще важна надёжность тестов? Причин несколько. Во-первых, с надёжными тестами мы можем быстрее выпустить релиз, и задача быстрее оказывается в проде. Во-вторых, мы смелее рефакторим: понимая, что тест надёжный и тылы прикрыты, мы можем обновить какие-нибудь библиотеки. И в-третьих, благодаря этому у нас будет меньше регрессии — тесты нас подстрахуют. Всё это в целом ведёт к экономии времени разработчика.

Как понять, что тест надёжный

Итак, мы написали наш новенький тест. Как узнать, что он стабилен? Для этого в Playwright есть удобная функция repeat each. Ставим, чтобы тест прогнался сто раз — число чисто эмпирическое, но вполне достаточное.

Добавляем прогону стрессовости: запрягаем все воркеры, доступные на машине. Для удобства анализа данных ставим репортер точками. И говорим Playwright, чтобы при первом отказе он заканчивал выполнение тестов.

Как выглядит код со всем этим, можно посмотреть с 03:45.

Кстати, этот же подход можно использовать, чтобы понять, стал ли тест быстрее после рефакторинга.

Чем плох хрупкий тест

Первая проблема, с которой мы сталкиваемся при хрупких тестах, — это явное ожидание. С 05:01 рассматривается типовой пример, когда мы открываем приложение, заполняем поля, но прежде чем заполнить, приходится ждать 5 секунд.

Пофиксить этот момент довольно просто: нужно довериться Playwright и использовать его API. Правки кода можно посмотреть с 5:55. С ними время можно сократить на 10 секунд.

Как добиться стабильности в скриншотных тестах

Сам по себе скриншотный тест выглядит достаточно просто (пример кода на 07:50): открыли страничку, вызвали toHaveScreenshot, Playwright сделал, сравнил и сказал, всё ли в порядке. Но если страница достаточно сильно нагружена, бывают проблемы — пример на 8:03. Из-за того, что у нас довольно нагруженный визуал, во время теста может отвалиться часть картинок. Поэтому отключить их на время теста нельзя. Мокать тоже не вариант — мы хотим понимать, что сохранятся пропорции картинок.

Решение достаточно простое: предзагружаем картинки, для этого можно написать утилитарную функцию и перед взятием скриншота её вызвать. Пример кода — на 8:55.

Ещё один нюанс — анимация. Playwright по дефолту отключает её, когда делает скриншот, но с бесконечной анимацией всё немного сложнее (пример на 10:25). Чтобы картинки в карусели не съезжали, просто прибиваем всё гвоздями к полу: останавливаем любые движения. Как это сделать в коде — на 10:43.

И последний нюанс — видео. Их при тестировании можно отключать, но нам важно понимать, что всё работает и, например, расширение видео читается браузером. Но при тестировании могут быть баги, как на 11:07, когда скриншот сделан, а видео успело немного проиграться. Это опять же несложно пофиксить: находим все видео, убираем атрибут autoplay, мотаем на самую первую секунду и ставим на паузу. Таким образом мы получаем стабильные скриншоты и одновременно понимаем, что видео на странице работает. Пример кода — на 11:13.

Надёжные тесты, которые падают, когда мы что-то сломали

Встречается такая типовая ошибка, как импорт кода приложения в тесты. Так делать не надо, потому что таким образом вы нарушаете принципы тестирования: вы начинаете проверять не само приложение, то есть то, что видит пользователь, а то, что «знает» разработчик. Тесты проверяют поведение приложения, а не внутреннюю реализацию. Кроме того, импорт кода создаёт ложную уверенность в работоспособности функционала.

Пример на 11:56. У нас есть страница бронирования отеля. Мы нажимаем на кнопку «Оплатить» и ожидаем, что нас перекинет на страницу успешной оплаты. Но тест, в котором мы используем пути из роутера, по сути ничего не проверяет. Мы можем поменять payment-success на что угодно, и тест будет проходить. Тесты должны иметь свои данные, с которыми они сравнивают поведение приложения.

Локаторы: ищем элемент как разработчик или как пользователь?

Если мы пишем локатор через getByTestId, у нас могут возникать проблемы:

  • Непонятно, что скрывается под локатором.
  • Не даёт информации о семантичности вёрстки.
  • Сложно соотнести в уме название локатора с элементом на странице без погружения в детали реализации.
  • Не отображает реального взаимодействия пользователя с приложением.
  • Требуется вручную покрывать компоненты атрибутами.
  • При рефакторинге легко потерять или перенести не на тот элемент.

В итоге мы не тестируем приложение как пользователь, а просто проверяем, как раскидали локаторы по проекту. На 13:41 можно посмотреть на код с такой проблемой. Причём тест пройдёт, но вряд ли именно такое ожидается от этого кода.

Современный подход предполагает использование локаторов через getByRole + text.

  • Даёт контекст, какой элемент скрывается под локатором.
  • Даёт информацию о семантичности вёрстки.
  • Легко читать тест.
  • Взаимодействуем с приложением как пользователь. Тестировщикам не нужно просить разработчиков добавлять атрибуты.
  • Рефакторинг не влияет на выполнение теста.

Типовой пример на 14:47. Даже если не знать, как выглядит домашняя страница OneTwoTrip, вы сможете прочитать этот тест — обладать знаниями о сервисе для этого не нужно.

Page Objects Models

Это по сути абстракция, которая даёт нам наружу методы и property страницы, через которые мы с этой страницей будем общаться. Есть некоторая мода хранить утверждения (expect) внутри методов именно в POMе. Чем это плохо? Когда мы пишем какой-то тест, мы этих экспектов не видим. И когда submitSearch вылетит, нам придётся выяснять саму реализацию POM — а это времязатратно. Код, о котором идёт речь, на 15:41.

Если вы всё же используете POM, то храните локаторы только в нём для удобного переиспользования и размещайте утверждения в тест-кейсах, а не в POM.

В целом можно выделить следующие причины не использовать POM:

  • Дополнительный уровень абстракции может усложнить понимание тестов и отладку (KISS > DRY).
  • Автогенерация локаторов через Playwright Codegen потребует значительных ручных доработок.
  • Без POM лучше изоляция тестов и меньше затрат на поддержку.
  • API Playwright уже предоставляет достаточный уровень абстракции.

Почему важно тестировать тесты

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

Если тест проходит при намеренно сломанном функционале, он неэффективен.

Пример ищите на 17:27. У нас есть сервис по аренде автомобилей, и раньше на странице был блок с логотипами наших партнёров. Мы их убрали, но тест остался и успешно проходил, потому что локатор висел просто на контейнере. То есть картинки снесли, контейнер остался.

Поэтому очень важно убедиться, что тест бывает не только зелёным, но и красным.

Выводы

Чтобы тест был стабильным, избегаем явных ожиданий, полноценно используем API Playwright. Предзагружаем и стабилизируем динамический контент при тестировании скриншотами. Проверяем тесты на стабильность через многократные запуски.

Для надёжности отказываемся от импорта исходного кода приложения в тесты. Используем локаторы getByRole + text вместо зависимых от DOM структуры. Тестируем с точки зрения пользователя, а не разработчика. И наконец, обязательно тестируем сами тесты, внося намеренные ошибки.

Чтобы тест был читаемым, нужно использовать локаторы, которые отражают пользовательский интерфейс (снова getByRole + text). Разумно применяет Page Objects Models и отдаём предпочтение простоте перед DRY. Не нужно из кодовой базы тестов делать второе предложение, которое придётся поддерживать.

1
Начать дискуссию