По мере роста продукта регрессионное тестирование быстро становится узким местом: количество сценариев растет, время проверки увеличивается, а цена ошибки перед релизом становится выше. В нашем случае переход к E2E-автотестам стал способом ускорить регресс и основой стабильных, предсказуемых релизов. В статье делимся тем, как мы выстроили покрытие регресса автотестами и встроили его в рабочие процессы команды.
Немного о проекте
Проект представляет собой распределенную систему, состоящую из двух web-порталов на React, порядка двадцати микросервисов на .NET и нескольких интеграций со сторонними системами. Все компоненты участвуют в одном сквозном бизнес-процессе, а релизы выходят регулярно — в среднем раз в две недели.
QA-инженер подключился к проекту уже после начала активной разработки. В этот момент мы осознанно отказались от наращивания объемной ручной тестовой документации и сделали ставку на E2E-автотесты.
Почему Е2Е?
Поддержание ручного регресса в актуальном состоянии задача важная для стабильного развития, но требует существенных затрат времени, вычитки и сверки с обновлениями. Часть кейсов теряют ценность, нужно время на их обнаружение. E2E-автотесты, напротив, становятся частью системы: они запускаются регулярно, отражают реальное состояние продукта и дают оперативный и понятный сигнал о готовности к релизу.
Для нас автотесты стали стратегическим инструментом. Они заменили собой классический ручной регресс и со временем начали выполнять роль индикатора качества — как для команды разработки, так и для менеджмента и заказчиков.
Выбор стека
Для написания E2E-тестов был выбран TypeScript. Это решение оказалось принципиальным: тот же язык используется во фронтенде, а значит автотесты стали понятны не только QA, но и разработчикам. Ревью тестов, обсуждение архитектуры и доработка сценариев перестали быть зоной ответственности только тестировщика.
В качестве фреймворка мы остановились на Cypress. Cypress хорошо интегрируется с CI/CD, имеет понятную документацию, активное сообщество и экосистему плагинов и расширений. За счет этого проще решать нестандартные задачи и развивать автотесты по мере роста продукта.
Для хранения тест-кейсов регресса и результатов прогонов мы использовали Team Foundation Server (Azure DevOps) — тот же инструмент, где уже находились код, задачи и документация по проекту. TFS это комбайн со встроенным GIT и TMS. И для данной задачи встроенного функционала возможностей оказалось достаточно для наших целей: кейсы связаны с PBI, имеют статус автоматизации и привязку к конкретным автотестам.
(выбор статуса автоматизации при создании кейса в тест-плане)
Как строилось покрытие регресса
После утверждения инструментов мы сформировали бэклог автоматизации, сосредоточившись на критичных пользовательских сценариях, которые уже реализованы и не подвержены частым изменениям. Эти сценарии легли в основу регрессионного тест-плана.
При этом ручное тестирование задач, идущих в релиз, всегда оставалось приоритетом. Мы исходили из простого принципа: автотесты не должны замедлять поставку на прод. Автоматизация развивалась постепенно — в начале спринта, в паузах между задачами, в моменты, когда ручная работа была временно заблокирована. Да, некоторые задачи на автоматизацию переносились между спринтами, но регресс при этом продолжал расти.
С самого первого теста автоматизация была встроена в CI. Для автотестов был настроен отдельный пайплайн с запуском после деплоя на тестовое окружение. Любой участник команды мог запустить его вручную, а результаты прогона автоматически возвращались в тест-план. Это позволило сразу сделать автотесты частью общего процесса.
(общий график с результатами прогона тестов в тест плане)
Примерно через четыре месяца автотесты полностью догнали существующий функционал. Перед релизами стало достаточно проанализировать результаты автоматического прогона, чтобы уверенно выпускать изменения в прод.
Когда регресс был полностью закрыт, фокус сместился на поддержку этого покрытия.
При тестировании новых задач автоматизация стала частью планирования. Если автотест не удавалось написать в рамках работы над PBI, чтобы не задерживать релиз, кейс фиксировался в тест-плане, а задача на автоматизацию возвращалась в бэклог. Это позволило сохранять баланс между скоростью поставки и качеством.
Автотесты для сложных сквозных сценариев
Иногда использовать только фреймворк для автотеста не подходит. Одним из показательных примеров стал сценарий создания коммерческого предложения на основе данных, взятых из письма. В нем участвуют несколько микросервисов. Обработка писем происходит в два этапа: сначала письма сохраняются в базе данных, затем парсятся и передаются в другой сервис для создания предложения. В сумме в обработке участвуют три последовательные job’ы, запускаемые по расписанию.
Поскольку в автотестах недопустимы явные ожидания (а помимо этого официальные рекомендации Cypress также предлагают избегать явных таймаутов), нам потребовалась помощь backend-разработчиков. Для тестов был реализован специальный метод, который последовательно запускает необходимые job’ы и возвращает результат выполнения.
В автотесте мы подготавливаем уникальные данные в базе, отправляем письмо с динамическими параметрами, запускаем обработку и проверяем, что в системе создано коммерческое предложение с корректными данными.
(код автотеста)
Такой сценарий в пайплайне выполняется менее чем за минуту, тогда как ручная проверка занимала около пяти минут сосредоточенной работы сразу в нескольких сервисах и не исключала человеческий фактор.
Оптимизация Cypress и поддерживаемость тестов
Cypress использует собственную модель асинхронности, не поддерживающую async/await, из-за чего при автоматизации легко столкнуться с так называемым callback hell — глубокой вложенностью then() и коллбэков.
С помощью экспертизы фронтенд-разработчиков мы решили эту проблему, реализовав базовый класс с использованием Proxy, который автоматически оборачивает вызовы методов в cy.then().
export class YieldBase {
constructor() {
return new Proxy(this, {
get(target, propKey, receiver) {
const targetValue = Reflect.get(target, propKey, receiver);
if (typeof targetValue !== 'function') {
return targetValue; }
return (...args: unknown[]) =>
cy.then(targetValue.bind(target, ...args));
},
});
}
}
Все page object’ы наследуются от YieldBase. Это позволяет выстраивать асинхронные операции в линейный и читаемый сценарий без явной вложенности.
Отдельного внимания требуют мутирующие команды, когда асинхронная операция изменяет состояние переданного объекта, а не возвращает значение напрямую, например при создании сущности в базе данных:
Cypress.Commands.add('createRequirement', (requirement) => {
databases.core
.insertInto('qt.Requirement')
.values(requirement.toDatabaseModel())
.returning('Id')
.executeQuery()
.then(({ rows }) => {
requirement.model.id = Number(rows[0].Id);
});
});
В таких случаях мы передаем объект целиком и обращаемся к измененным данным после выполнения команды. Даже при наличии подобных операций код остается читаемым и последовательным.
Результаты автоматизации регресса
Что мы получили за счет такой атвоматизации:
- автоматизация регресса позволила проходить его в десятки раз быстрее и повысить его эффективность.
- использование общего для команды технологического стека повысило прозрачность и вовлеченность,
- высокий процент дефектов, обнаруживаемых автотестами, подтверждает их критическую роль в обеспечении качества продукта.
На данный момент автоматизировано 145 E2E-сценариев разной сложности. Если в среднем брать около трех минут на ручное прохождение одного кейса, полный регресс занял бы целый рабочий день тестировщика. Прогон автотестов в пайплайне длится около 25 минут. Автотесты используются как предрелизный регресс и находят до 20% всех дефектов на тестовом окружении в регрессе. Согласно World Quality Report, зрелость автоматизации напрямую влияет на снижение количества дефектов, обнаруживаемых после релиза.
(диаграмма багов, обнаруженных на тестовом окружении в разбивке по способу обнаружения)