- Две оси E2E
- Подход Given-When-Then
- Параллелизация
- Смягчение привязки к имплементации
- Что делать с пользовательскими E2E
“Медленные и ненадежные тесты — бич QA-отдела, они препятствуют внедрению передовых практик, например Trunk-Based-Development и непрерывной интеграции. Во многих современных проектах на выполнение тестовых наборов уходит более 30 минут, а случайные сбои требуют перезапуска набора, замедляя и без того медленное выполнение.
В своей предыдущей статье я поделился стратегией, позволяющей “продвинуть” большинство тестов вниз по пирамиде, с минимальным влиянием на качество и покрытие. Однако вам 100% будут нужны и сквозные тесты, потому что они необходимы для проверки того, что система действительно работает, когда это наиболее важно: когда с ней взаимодействуют пользователи. Проблема в том, как сделать так, чтобы эти сквозные тесты не превращали CI-конвейеры в просто «I» (не непрерывные).
В этой статье я поделюсь своими 3 правилами написания “молниеносных” сквозных тестов, а также реальным бенчмарком, показывающим, насколько быстрыми они могут быть.
Две оси E2E
Первый шаг к предотвращению замедления и хрупкости — это признание того, что существует два разных взгляда на то, что означает термин «end-to-end»:
- Пользовательский e2e-поток: Верифицирует поток пользователя. В электронном магазине, например, это вход в систему, поиск товара, добавление его в корзину, оформление заказа и получение подтверждения.
- Системный e2e-поток: Верифицирует одно поведение, проходя всю систему (без моков). Например, один тест будет проверять, что логин работает, другой — функциональность поиска и т. д., и каждый из этих тестов будет использовать реального провайдера аутентификации, реальную базу данных и реальный пользовательский интерфейс — точно так же, как это делал бы реальный пользователь.
Хотя пользовательские E2E-тесты имеют свое законное место в стратегии тестирования, они по своей природе медленные и хрупкие. А вот системные E2E-тесты могут быть удивительно быстрыми. Именно поэтому мое первое правило гласит:
Правило №1: В своем CI-пайплайне фокусируйтесь на системном E2E — а не пользовательском.
Когда мы говорим о тестах нижнего уровня, мы, естественно, фокусируемся на Системной Оси: в юнит-тесте мы тестируем один компонент системы (отдельную функцию или класс) за один раз, а остальные полностью мокируем. В интеграционных тестах мы проверяем одну функциональность за раз, обрабатываемую несколькими компонентами системы вместе (например, прямой вызов конечной точки API с подключенной базой данных).
В обоих случаях мы проверяем один сценарий за раз — один вызов функции или один вызов API — а не поток. Мы различаем Unit и Integration на Системной Оси, а не на Пользовательской.
Но при переходе к E2E-части пирамиды мы часто скользим вперед по Системной Оси , но при этом внезапно совершаем неявный прыжок по Пользовательской в то же самое время, написав тесты, которые проверяют не только всю систему, но и весь пользовательский поток.
Обратите внимание, что я не призываю полностью отказаться от пользовательских E2E-тестов — и позже я расскажу о том, какое место они должны занимать в вашей стратегии.
Два типа сквозных тестов. Красная стрелка обозначает «прыжок»: переход к E2E-тестам, которые проверяют потоки целиком, пропуская E2E-тесты, которые проверяют одно поведение за раз, как это делают юнит- и интеграционные тесты.
Мышление по принципу Given-When-Then
Правило № 1 устанавливает, что тесты системные E2E-тесты проверяют по одной функции за раз. Для этого каждый тест должен иметь «эталонное состояние» системы. На примере сайта электронной коммерции у нас будет один тест для входа в систему, другой — для поиска товара, третий — для добавления его в корзину и третий — для оформления заказа. Проблема в том, что большинство этих тестов требуют, чтобы система находилась в определенном состоянии, например, когда пользователь уже вошел в систему. Как добиться этого для каждого теста?
Самый очевидный способ — использовать UI для входа в систему, как мы это делали при тестировании формы входа. Но у этого подхода есть серьезные недостатки:
- Очень медленный. Для любых фич, кроме самых простых, практически все время выполнения каждого теста будет уходить на настройку, а не на тестирование.
- Хрупкий. Чем больше действий выполняется через пользовательский интерфейс, тем больше возможностей для того, чтобы тест стал хрупким. Время от времени настройка интерфейса будет давать сбои, подрывая доверие к тестовому набору.
- Очень избыточный. Тестируя по одной сущности за раз, мы стремимся к состоянию, когда одна сломанная фича означает один сломанный тест — так мы можем легко определить проблемы, когда они возникают. Если, например, большинство наших тестов используют пользовательский интерфейс для входа в систему, то неработающая форма входа приведет к сбою многих не связанных с ней тестов, что замаскирует информацию о том, где находится проблема. Мы хотим, чтобы связь между ошибками и отказами тестов была как можно более ясной.
Атомарные тесты (те, которые проверяют одну вещь за тест) состоят из 3 частей. В терминах Gherkin это:
Given: Настройте систему так, чтобы она находилась в состоянии, необходимом для нашего теста
When: Активируйте систему одним действием (или как можно меньше действий)
Then: Проверьте, соответствует ли новое состояние системы тому, что мы ожидали от действия.
Правило #2: Выполняйте этап «Настройки» («Given») с помощью быстрых программных вызовов.
Ключом к правилу №2 является понимание того, что Given-часть на самом деле не является частью теста — это просто настройка. Мы хотим, чтобы настройка выполнялась как можно быстрее. Для этого мы будем использовать программный доступ к нашей системе (аутентификация, заполнение БД и т. д.), чтобы наши тесты тратили как можно меньше времени на настройку.
Например, для теста оформления заказа нам нужен авторизованный пользователь с товаром в корзине. Это наш Given-этап. Чтобы привести систему в такое состояние, мы сначала выполним программный вызов нашего Auth-провайдера для создания сессии входа с токеном, а затем вызов бэкенда с этим токеном для добавления товара в корзину вошедшего пользователя. Эти вызовы будут на порядки быстрее, чем использование пользовательского интерфейса для достижения тех же результатов.
Затем мы выполняем тест и проверяем его с помощью пользовательского интерфейса, чтобы убедиться, что система находится в предполагаемом новом состоянии (части теста When/Then).
Иллюстрация того, какую часть теста мы выполняем через API, а какую — через UI. Текст теста взят из: https://www.subject7.com/gherkin-behavior-driven-testing-hype-or-not/.
Как может выглядеть системный сквозной тест в Playwright
Параллелизация
Точно так же, как мы стремимся, чтобы наши модульные и интеграционные тесты были изолированы, выполнялись в любом нужном порядке и параллельно, то же самое должно относиться и к нашим системным E2E-тестам. При полностью распараллеленных тестах вы можете легко сократить общее время выполнения, добавив больше worker’ов.
Правило № 3: Сделайте системные E2E-тесты параллельными.
Тест-кейс реального мира
Даже при соблюдении всех вышеперечисленных правил время работы CI в одном из моих проектов (приложение Expo) начало приближаться к 10 минутам на одном Github Runner, что делает работу Trunk-Based и Atomic Commits медленной и неудобной. Поскольку на написание одного коммита у меня уходит около 3-5 минут, а CI работает в два раза дольше, я начал сомневаться в том, стоит ли сразу пушить каждый свой коммит, что стало тревожным сигналом.
К счастью, мы с самого начала ориентировали свои системные E2E-тесты на параллельное выполнение, поэтому переход на собственный хостинг с несколькими worker’ами был быстрым и безболезненным, и значительно уменьшил время выполнения (как приятный бонус, собственный хостинг избавляет от необходимости переустанавливать статические зависимости для каждого запуска, такие как docker, браузеры Playwright и т. д., что еще больше сокращает общее время прохода через CI).
Вот бенчмарк Playwright, выполняющий 80 тестов системных E2E-тестов:
Github Runner:
1 worker: Всего: 8:30 минут, Тесты: 5:50 минут.
Собственный раннер:
1 worker: Всего: 6:53, Тесты: 5 минут.
2 worker’а: Всего: 4:12, Тесты: 3 минуты.
3 worker’а: Всего: 3:22, Тесты: 2 минуты.
4 worker’а: Всего: 2:30, тесты: 1,4 минуты.
(Примечание: при использовании 4 worker’а тесты выполнялись настолько быстро, что Auth-провайдер иногда отклонял вызовы из-за ограничения скорости, что приводило к периодическому увеличению времени выполнения, поэтому мы остановились на 3 worker’ах. Устранение одного узкого места может привести к появлению другого, часто неожиданного, поэтому бенчмаркинг очень важен).
Бенчмарк 80 E2E-тестов с 1-3 worker’ами
Чем быстрее работает CI, тем более детализированными могут быть ваши действия, позволяя выявлять ошибки и проблемы интеграции до того, как они успеют накопиться.
Смягчение привязки к имплементации
Один из минусов следования этим правилам заключается в том, что мы отказываемся от важного принципа BDD — что наши тесты не срабатывают только при изменении поведения, а не при изменении деталей реализации. Делая прямые вызовы к нашим API, мы подвергаем наши тесты риску сбоя при модификации или изменении этих API, что не является идеальным. По сути, чтобы ускорить этап «Given«, мы привязываем его к архитектуре.
У каждого подхода есть плюсы и минусы, и если мы получаем молниеносно быстрые тесты, я бы назвал это справедливым компромиссом. Однако мы можем смягчить этот недостаток, добавив слой абстракции между тестами и архитектурой — например, для действий аутентификации есть класс-хелпер, который взаимодействует с провайдером аутентификации и вызывается тестами. Если мы изменим провайдера аутентификации, у нас будет только одно место для исправления, вместо того чтобы проходить через все тесты и исправлять их по одному.
Что делать с пользовательскими E2E-тестами
Чтобы убедиться в том, что наше приложение обеспечивает хороший пользовательский опыт, нам по-прежнему нужны тесты, которые выполняют более длительный поток. К сожалению, такая проверка медленная и хрупкая по своей природе. Поэтому я бы избегал включать пользовательские E2E-тесты в обычный CI-пайплайн.
Вместо этого рассмотрите следующие варианты:
- Каждый день: Планируйте эти тесты раз в день, желательно с достаточным количеством повторных попыток, чтобы свести к минимуму влияние флуктуаций, связанных с UI.
- Перед развертыванием: Запускайте их перед развертыванием.
- После развертывания: Если вы практикуете непрерывное развертывание (CD), рассмотрите возможность запускать их после развертывания, с автоматическим откатом в случае неудачи.
Бонусное правило: Не включайте в CI-пайплайн медленные по своей природе пользовательские E2E-тесты.
Исключив эти тесты из CI, вы ускоряете повседневную разработку и при этом получаете преимущества от полной проверки пользовательского потока регулярными интервалами и в нужные моменты.
Быстрый и надежный набор системных E2E-тестов обеспечивает большую часть уверенности, а пользовательские E2E-тесты ставят окончательную печать одобрения, не становясь при этом узким местом.
Резюме
Скорость, надежность и покрытие при тестировании напрямую влияют на скорость разработки и качество ПО. К сожалению, E2E-тесты часто становятся проблемной частью конвейера. Но если следовать простым принципам, знакомым по модульным и интеграционным тестам, можно сделать свои E2E-тесты удивительно быстрыми и стабильными. Итак, еще раз:
- Сфокусируйте свой CI-конвейер на системных E2E-тестах, которые проверяют по одной фиче за раз.
- Ускорьте их настройку с помощью programmatic-вызовов.
- Выполняйте их параллельно.
- Бонусное правило: Переведите более длинный поток пользовательских E2E-тестов в pre/post-deployment запуски.
Следуя этим правилам, вы сохраните и скорость, и качество, гарантируя, что E2E-тестирование будет поддерживать ваш процесс разработки, а не мешать ему.”