Русский
Русский
English
Статистика
Реклама

Autotest

Никогда не писали автотесты? Попробуйте Cypress

28.09.2020 20:13:28 | Автор: admin

Никогда не писали автотесты? Попробуйте Cypress


Автотесты на Cypress
Первое впечатление и встречающиеся проблемы


Дмитрий Кочергин, Lead Software Developer Luxoft

Прежде всего хочу отметить, что я не профессиональный автотестер, а Java-программист. Но однажды поступила задача сделать smoke тесты для базовой проверки основных функций приложения, которое недавно поломали изменением в API зависимого сервиса без предупреждения.

Статья будет интересна всем, кто интересовался автотестированием с нуля на JS, но боялся спросить.

Начну с того, что по автотестированию я ничего никогда не читал. Основной преконсепшн был, что автотесты это сложно, потому что по отзывам пока этот Selenium настроишь Плюс надо учить XPath и PageObject, чего делать вообще не хотелось.

Быстрый поиск по интернету дал более молодые и перспективные инструменты: WebDriver.IO, Pupeteer (а сейчас лучше Playwright) и Cypress. Выбрал последний, купился на красивые обещания и несколько комментов из холиваров по лучшим инструментам для автотестов.

Так выглядит окно браузера запущенного теста. Слева выполненные команды теста и статус, справа просмотр приложение во время исполнения теста:
image

Вот так выглядит код теста (в Cypress весь код на JS, а селекторы это обычные CSS селекторы):
image

В runtime выглядит так:
image

Пока искал инструмент для автотестов в голове у меня была картинка, что тесты можно создавать прямо в браузере, чтобы просто нажал на запись скрипта система записала мои действия (CSS селекторы элементов, на которые я нажимал). Потом я смотрю сгенерированный тест, подправляю селекторы если нужно, и сохраняю в Test Suite. Эх

Сказка оказалась тогда недостижимой и таких инструментов я не нашел (возможно кто-то в комментах подскажет правильный путь). Но в Cypress подкупило то, что тесты выполняются в настоящем браузере, и даже можно параллельно с выполнением теста исследовать DOM любимыми инструментами разработчика Chrome (если например селектор не сработал можно открыть консоль и сразу при исполнении теста посмотреть почему).
image

Из других параметров, которые для меня были важны я критически не хотел тратить много времени ни на написание (опять про запись теста в браузере), ни на поддержку тестов. Для моих целей этого было вполне достаточно. Просто для информации o Cypress: с момента, как я впервые открыл сайт Cypress, до первого теста, который логинится в приложение прошел 1 час.

Итак, Cypress, первая страница фреймворка говорит нам что это JavaScript End to End Testing Framework (cypress.io). Далее бегло читаем документацию, она действительно полная, и можно найти ответы практически на все вопросы (все остальное я быстро находил на StackOverflow):
image

Дальше по списку фичей с сайта:
  • Time travel каждый шаг выполнения теста в консоли можно нажать и вернуться на конкретное состояние всего приложения в прошлом, которое отображается прямо в браузере. И это не просто записанная картинка, а реальный DOM, можно откатиться и исследовать страницу через Chrome devtools.
  • Real time reloads как во всей модерн JS теме, поменяли исходники в тот же момент произошёл перезапуск теста в браузере (hot reload).
  • Automatic waiting многие инструкции теста асинхронные, как то переход на страницу, и Cypress сам автоматически дожидается окончания загрузки. Кроме конечно момента, когда вызовы сервера делает приложение.
  • Network traffic control Cypress может захватывать/видеть вызовы сервера и можно задать инструкцию, чтобы подождать ответ от сервера.
  • Screenshots and videos во время выполнения теста Cypress записывает видео экрана (MP4) браузера, вместе с инструкциями в окне.

Все это добро конечно же можно запускать и без открытого браузера на CI, используется headless Chrome или Electron.

Но были и некоторые проблемы.

По началу я не знал, что можно ждать окончания XHR запросов на сервер, и вставлял вместо них timeouts, и тесты рандомно падали. Поправил.
image

Потом через раз работал Hot Reload, постоянно приходилось перезапускать весь Cypress, потому что не было уверенности, что мои изменения применились. Оказалось, что в моей IDE (IntelliJ IDEA) есть такие нехорошие галочки, которые еще и включены по умолчанию, из-за которых получается что сохранение файла это не сохранение, а eventual сохранение.
image

Следующая проблема была в том, что мое приложение использовало window fetch для запросов на сервер, а Cypress видит только XHR запросы. Dirty hack из StackOverflow помог (я так понял, что метод fetch удаляется из window, чтобы браузер сделал fallback на XHR вместо fetch):
image

Далее встала проблема эмуляции мобильного браузера, просто в коде теста user agent перезаписать не получилось, но в отдельном специальном файле все получилось.
image

Далее решение CORSпроблемы:
image

Потом file upload, с наскоку не получилось, стандартные решения не сработали, зато помогла библиотека cypress-file-upload:
image

Единственная проблема которую я не смог решить это воспроизводимость теста. А именно стабильные и одинаковые начальные данные для запуска теста (fixtures), это более конфигурационная задача, а не Cypress, но все равно пока нерешенная.

В итоге, Cypress выглядит отличным инструментом для внедрения автотестирования в проект, если:
  1. Есть знание JS
  2. Нет необходимости тестировать во всех видах браузеров начиная с IE6 (на данный момент Cypress поддерживает Chrome, Chromium, Edge, Electron, Firefox). Вот обсуждение темы. Но могу сказать, что год назад, когда я начинал работать с Cypress он поддерживал только последнюю версию Chrome и Electron для запусков без UI.
  3. Хотите быстро сделать тесты и забыть о них, пока кто-то не сломает приложение :)

Cypress: берите и пользуйтесь!
Подробнее..

Cypress и его место в нашей тестовой пирамиде

18.05.2021 08:17:48 | Автор: admin

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

Введение в Cypress

Если отбросить капитанское определение, что Cypress это JavaScript-фреймворк для тестирования, то важно отметить, что при работе с ним мы видим на экране браузер. Он не обязательно открыт, он может быть headless, но он есть, и он открывает особое приложение самого Cypress, которое состоит из нескольких фреймов: в одном фрейме открывается продукт, который мы тестируем, в другом фрейме запускаются тесты. Код тестов пишется на JavaScript, поэтому они могут выполняться непосредственно в браузере ведь это нативный для него язык.

Так с помощью JavaScript API производятся все манипуляции, которые делаются в тестах, то есть заполнение форм, клики и тому подобное.

Преимущества Cypress

Нет Selenium WebDriver

Очевидное отличие Cypress от тех библиотек и фреймворков, которые мы использовали раньше это отсутствие основного действующего элемента, Selenium.

Selenium WebDriver это third-party сервис на Java, который обращается к браузеру по WebDriver протоколу. Это накладывает ограничения на работу с браузером в рамках протокола. Сетевое взаимодействие также вносит свой вклад во время выполнения тестов.

Изначально Selenium был создан не специально для тестов, а как общий инструмент автоматизации для браузера. Cypress, в отличие от него, сфокусирован на решении конкретной задачи, а именно, на создании end-to-end (е2е) тестов для интерфейса web-приложений.

Все в одном

Cypress не нужно собирать из кусочков он принес все достаточно современные "батарейки" с собой:

  • Синтаксис BDD (унаследовано из Mocha): describe(), context(), it().
    А также хуки: before(), beforeEach().
    Использовать такой DSL привычно для тех, кто уже писал юнит-тесты на JavaScript.

  • Библиотека ассертов (унаследовано из Chai). Например:
    expect(name).to.not.equal("Jane") ожидание того, что элемент не существует это не то же самое, что ожидание неудачи при проверке существования элемента. Если элемента нет, то это хорошо, это не нужно перепроверять, а нужно идти дальше.
    Такую задачу должен решать тестовый фреймворк, и этого нам очень не хватало в старой самописной библиотеке, при использовании которой многое ложится на плечи разработчика теста.

  • Перехват, отслеживание (spy) и подмена (mock) запросов браузера к бэкенду.

Development experience

Главное преимущество Cypress это отличный development experience. Написать первый тест для своего проекта (неважно, на каком языке написан сам проект) можно минут за 10. Потребуется добавить одну зависимость в package.json (npm install cypress), прочитать документацию про то, куда складывать файлы (cypress/integration/login.spec.js), и написать код в 5 строчек:

describe('Login', () => {it('should log in with credentials', () => {cy.visit('/login');cy.get('[name=login_name]').type(Cypress.env('login'));cy.get('[name=passwd]').type(Cypress.env('password'));cy.get('[name=send]').click();cy.get('.main-header').should('be.visible');});});

Получаем реальный тест, который посещает страницу логина, заполняет форму, нажимает на кнопку и видит результат.

На скриншоте браузера было видно, что все шаги теста логируются. Но это не просто лог это навигация, по которой после прохождения теста можно вернуться в любую точку и увидеть, что происходило в браузере. Например, можно посмотреть снэпшоты до и после Ajax-запроса.

Приятной мелочью является то, что каждый cy.get() убеждается, что страница загрузилась, и делает несколько попыток, чтобы найти элемент. С каждым годом интерфейсы веб-приложений становятся все сложнее. Результирующий HTML формируется не на стороне сервера, а на стороне браузера. Делается это асинхронно и с использованием различных библиотек компонентов. В какой момент тот или иной элемент интерфейса появится на экране, сказать уже становится сложнее.

Одна из Best Practices говорит, что не нужно никогда писать таймаут типа "подождать 2 секунды". Абсолютно все таймауты должны ждать чего-то осязаемого, например, окончания Ajax-запроса. Можно подписаться на событие, которое случается в коде продукта. Например, когда нам через веб-сокет прилетает событие с бэкенда, то срабатывает определенный listener на фронтенде.

Вся документация Cypress и Best Practices находятся на одном сайте docs.cypress.io хотелось бы отдельно отметить высокое качество этой документации, а также мастер классов, которые команда разработки Cypress проводит и публикует в открытом доступе.

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

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

Тестовая пирамида

Когда говорят про тестовую пирамиду, то обычно приводят в пример анти-паттерн "перевернутая пирамида" или "стаканчик мороженого". То есть на нижнем уровне в таком примере количество юнит тестов стремится к нулю. Лично мне этот случай кажется невероятным для зрелого проекта: ведь в этом случае разработчики должны были полность отказаться писать самые простейшие тесты откуда тогда взялись сложные е2е тесты?

Как бы то ни было, к нам это не относится у нас несколько тысяч PHPUnit-тестов с покрытием около 12% строк кода.

В то же время у нас есть еще несколько тысяч е2е-тестов с Selenium, которые проверяют все возможные конфигурации продукта, занимают кучу времени (подмножество, запускаемое на каждый коммит, мы смогли оптимизировать до 40-60 минут), имеют довольно слабый уровень доверия (с вероятностью 30-40% тесты упадут, хотя коммит не содержит причины этого падения) и покрывают около 30% строк кода.

Получается, наше положение выглядит, как песочные часы нам не хватает среднего слоя в тестировании, где интеграционные тесты проверяют компоненты системы независимо друг от друга. Это горлышко песочных часов и хочется заполнить с помощью Cypress. При этом еще хочется что-то сделать с существующими е2е тестами, чтобы "заострить" вершину пирамиды. То есть, важный акцент здесь в том, что Cypress не является заместителем старого фреймворка: мы не хотим просто взять и переписать все тесты на Cypress иначе мы так и останемся на шарике мороженого. Цель тестов по-прежнему, проверять регрессию в продукте, но проверять на другом уровне, чтобы выполняться быстрее и получать результат раньше, а также быть легче в сопровождении.

Наш подход к написанию тестов

Проект, о котором идет речь, это контрольная панель Plesk. Она предоставляет пользователям интерфейс для управления хостингом веб сайтов. Функциональность панели доступна не только через UI, но и через API и CLI, которые используются для автоматизации.

Мы начали с того, что сделали следующие предположения:

  • Тесты на Cypress относятся чисто к UI. Мы не относим сюда тесты, у которых шаги выполняются через API или CLI.

  • Мы не проводим никакой дополнительной валидации, кроме той, что выполняется средствами UI. Например, если мы проверяем создание домена, то мы не отправляем запросы для проверки Web-сервера или DNS, мы считаем тест пройденным, если в UI появилось сообщение на зеленом фоне о том, что домен создан успешно. Такой подход избавляет нас от предварительной подготовки и написания тестовых сценариев.

  • На первом этапе мы автоматизируем только позитивные сценарии. Негативные сценарии не представляют ценности для клиента, но при этом тесты для их проверки занимают драгоценное время. Так что переносим такие сценарии в нижнюю часть пирамиды они, как правило, легко проверяются юнит-тестами.

Наш опыт работы с Cypress в сочетании с официальными рекомендациями привел нас к использованию следующего набора практик:

Сбрасывать состояние продукта

Мы сбрасываем состояние продукта до исходного перед запуском каждого набора тестов (Cypress рекомендует делать это перед запуском каждого теста, но мы используем облегченный вариант). Мы создаем дамп базы данных и восстанавливаем его перед прогоном каждого набора тестов (test suite / spec). Это занимает порядка 5 секунд.

before(cy.resetInstance);//=> test_helper --reset-instance//=> cat /var/lib/psa/dumps/snapshot.sql | mysql

Такой откат к прежнему состоянию может в общем случае привести Plesk в нерабочее состояние, т.к. помимо общей базы есть еще, например, базы отдельных утилит, а также бэкапы, которые хранятся в файловой системе. Но нас это устраивает, т.к. мы используем Cypress только для тестирования UI.

Использовать фикстуры

Вместо реальных объектов в качестве пререквизитов теста мы используем фикстуры сохраненные структуры, которые содержат необходимое состояние базы данных. Например, для выполнения некоторых тестов необходимо наличие домена. Вместо создания реального домена мы воссоздаем все необходимые записи в базе данных, но не трогаем файловую систему и другие системные сервисы. Это занимает меньше секунды (для сравнения, создание полноценного домена заняло бы около 30 секунд).

cy.setupData(subscription).as('subscription');//=> test_helper --setup-data < {domains: [{ id: 1, name: "example.com" }]}

Такие объекты не будут выполнять полноценные пользовательские сценарии, но для тестирования UI их будет достаточно.

Использовать прямые URL

Мы не используем навигацию и попадаем в нужные места UI по прямым URL-ам. Мы вызываем свою специальную команду login, которая создает сессию, а затем переходим прямо на нужную страницу.

beforeEach(() => {cy.login();cy.visit('/admin/my-profile/');});

В старом фреймворке мы бы использовали PageObject для входа в главное меню, а затем переходили бы из него к нужному элементу. Здесь же этого не требуется, так как мы тестируем только необходимую страницу. Единственное дублирование это команда login, но это не выглядит проблемой.

Фронтенд без бэкенда

Иногда бывает сложно создать условия для определенного состояния, которое мы хотим протестировать. Например, для проверки доступных обновлений. Намного легче подсунуть заготовленный ответ для Ajax-запроса, чем готовить инфраструктуру для обновлений.

const lastChecked = 'Jan 29, 2021 04:42 PM';cy.intercept('POST', '/admin/home/check-for-updates', {status: 'success',lastChecked,newVersion: null,whatsNewUrl: null,}).as('checkForUpdates');cy.get('[data-name="checkForUpdates"]').click();cy.wait('@checkForUpdates');cy.get('[data-name="lastCheckedDate"]').should('contain', lastChecked);

Пока не все данные доставляются через Ajax, а фронтенд не является полноценным SPA, но мы движемся в этом направлении. И именно такой подход в тестировании фронтенда с помощью заготовленных ответов бэкенда кажется нам наиболее перспективным, так как он позволит вообще не запускать бэкенд и ускорить выполнение тестов.

Стабильность тестов

Скорее всего, когда вы начнете писать Cypress тесты, то каждый второй тест будет неожиданно становиться flaky, то есть иногда проходить, а иногда нет. Чтобы избежать такой нестабильности, мы используем следующие практики.

Дожидаться выполнения Ajax-запроса

Многие формы в нашем продукте отправляются с помощью Ajax-запросов без перехода страницы. Чтобы тест гарантированно прошел, необходимо перехватить этот запрос и дождаться его завершения. Так как в Cypress мы проверяем только то, что происходит в UI, мы дожидаемся нужного нам сообщения.

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

cy.intercept('POST', '/admin/customer/create').as('customerCreate');cy.get('[name=send]').click();cy.wait('@customerCreate');cy.get('.msg-box.msg-info').should('be.visible');

Дожидаться исчезновения индикатора загрузки

Кое-где в нашем интерфейсе фоновые операции, например, обновление списка, сопровождаются анимированным индикатором загрузки ("крутилкой"). Именно на таких страницах после окончания Ajax-запроса случается ошибка "element has been detached from the DOM" при попытке Cypress кликнуть на элементы списка. Поэтому мы добавляем после Ajax-запроса дополнительную строку, которая проверяет, что индикатор загрузки не виден.

cy.get('.ajax-loading').should('not.be.visible');

Мы надеемся, что проблема будет исправлена на стороне Cypress и нам больше не придется за этим следить.

Ajax-запросы после окончания теста

После окончания каждого теста браузер остается на последней открытой странице. Cypress сбрасывает состояние браузера (включая куки) и запускает следующий тест, где первым шагом мы сбрасываем состояние продукта (включая сессии бэкенда).

До того момента, когда следующий тест сделает первый вызов "cy.visit()", предыдущая страница остается открытой и может отправлять Ajax-запросы (например, периодическое обновление), которые будут падать из-за ошибки авторизации (куки нет, сессии нет).

В качестве workaround можно переходить на пустую страницу, чтобы браузер сбрасывал все активные Ajax-запросы. Для этого добавляем в support/index.js

afterEach(() => {cy.window().then(win => {win.location.href = 'about:blank';});});

Первые результаты

За 3 человеко-месяца (3 итерации) мы получили следующие результаты:

  • 335 тестов на Cypress (разбиты на 84 спеки)

  • Пайплайн полностью выполняется за 35-40 минут, из которых сами тесты занимают 20 минут

  • Запуск пайплайна на каждый пулл-реквест в блокирующем режиме (то есть нельзя мержить без успешного прохождения тестов)

  • Уровень доверия выше 95% (то есть вероятность flaky падения ниже 5%)

  • Покрытие интерфейса 35% (ниже расскажу подробнее)

Пайплайн для запуска тестов

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

Как и для большинства наших задач, пайплайн запускается в Jenkins и хранится в Jenkinsfile вместе с кодом проекта.

Линейный пайплайн

В первом заходе получился простой линейный пайплайн.

Мы запускаем Docker-контейнер с Plesk в фоновом режиме и ждем, когда он будет доступен в локальной сети. Потом запускаем другой контейнер с Cypress и кодом тестов, он подключается к Plesk и выполняет все тесты, а мы ждем его завершения (не делаем detach).

Мы запускали тесты на машине с 12 ядрами, которая используется у нас для сборки Plesk и ряда его служб. В течении рабочего дня у нас бывает до 20-30 сборок. В результате Load Average достигал 20, и многие соседние процессы "вставали". Мы добавили ограничение на количество исполняемых сборок до 3-5. Но и этого оказалось недостаточно, соседи по железу продолжали жаловаться на нагрузку.

Тогда мы унесли запуск тестов на выделенный сервер в AWS с 4 ядрами внутри VPC с доступом в нашу офисную сеть. Тем самым мы избавились от недовольных соседей, но сборки тестов так и продолжили подолгу ждать в очереди, время от времени не укладываясь в таймаут.

Пайплайн с параллельными шагами

Чтобы как-то ускорить процесс, мы решили воспользоваться Jenkins EC2 Fleet plugin, который предоставляет Jenkins slave ноду по требованию из Autoscaling Group в AWS и уничтожает неактивные ноды после некоторого простоя. Такой подход позволяет тратить деньги на аренду ресурсов только тогда, когда они необходимы.

Переход на spot-инстансы позволил нам существенно сэкономить: вместо $150 в месяц за ondemand c5.xlarge, мы стали тратить около $60 за c5.xlarge и более мощные c5.2xlarge.

А главное, мы можем делать столько одновременных запусков, сколько нам нужно.

Разворачивание новой ноды занимает порядка 2 минут. Мы сделали в пайплайне несколько шагов параллельными, чтобы за это время успеть собрать продукт и быть готовыми к его установке в Docker на новой ноде.

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

Пайплайн с параллельными тестами

В Cypress есть платная фича параллельный запуск тестов с помощью Cypress Dashboard. Но мы пошли простым и бесплатным путем перечисляем файлы с тестами при запуске контейнера, при этом первый запускает все четные файлы, второй все нечетные.

cypress run --spec $(find 'cypress/integration' -type f -name '*.js' | awk '(NR - ${RUNNER}) % ${TOTAL_RUNNERS} == 0' | tr '\n' ',')

Получилась матричная сборка, где для каждой оси (axis) запускается свой контейнер с Plesk и свой контейнер с определенным набором тестов.

В итоге мы укладываемся в приемлемые 35-40 минут для всего пайплайна, а время одной пачки тестов занимает примерно 20 минут.

В дальнейшем с увеличением количества тестов нам, очевидно, потребуется больше параллельных потоков, и мы планируем использовать Amazon ECS для запуска контейнеров, чтобы не ограничиваться EC2 инстансом.

Измерение URL coverage

В нашем проекте используется много различных языков программирования и анализ code coverage является больной темой, потому что сбор данных требует специальных билдов и объединения отчетов с нескольких конфигураций.

Для анализа тестового покрытия UI мы решили воспользоваться продуктовой аналитикой и сравнить данные, полученные от тестовых инсталляций, с данными от реальных пользователей. У нас уже был сервис, аналогичный Google Analytics, для сбора пользовательских метрик, а тестовые данные складывались отдельно и никем не использовались. Из множества метрик мы отфильтровали события о посещенных URL-ах (страницах) продукта, начали сохранять эти данные в удобном для нас виде в базу данных и составлять отчет по посещенным адресам.

По полученным данным, за счет всего автоматического и ручного тестирования внутри компании мы покрываем около 60% URL-ов, которые посещают реальные пользователи в течении месяца. Наши старые тесты покрывают около 25%, а новые тесты на Cypress уже достигли 35%.

Эта статистика помогает нам планировать дальнейшее тестирование например, более посещаемые страницы мы планируем автоматизировать в первую очередь.

Следующие шаги

Ускорить сборку Docker

Одна из проблем, над которой мы хотим поработать ускорение сборки контейнеров Docker. Как уже было сказано выше, мы создаем временный сервер в AWS (slave node) для каждой сборки Docker, и эта сборка на данный момент занимает в среднем 8 минут. Но поскольку каждый временный сервер новый, то мы совершенно не используем преимущества кэширования, а хотелось бы ими воспользоваться. Поэтому сейчас мы исследуем возможность использования BuildKit. Альтернативными решениями могут стать Kaniko или AWS CodeBuild.

Сократить количество е2е тестов

Мы хотим уменьшить количество старых е2е тестов, чтобы вершина тестовой пирамиды стала более острой. Сейчас около трети этих тестов относятся к UI. Кроме этого, тесты содержат проверки через командную строку (CLI), причем иногда логика тестов в UI и CLI дублируется.

Основная идея: перенести все UI-тесты в Cypress, а в старом фреймворке оставить только CLI-тесты с детальными проверками. Поэтому для каждого UI-теста из старого фреймворка мы делаем следующее:

  1. Заменяем UI-шаги на CLI (если это возможно).

  2. Удаляем, если уже есть аналогичный тест с CLI.

  3. Если проверка возможна только через UI уносим ее в Cypress.

Например, при создании домена проверяется то, что он резолвится, и что на нем работают определенные скрипты. Эти проверки останутся только для создания домена через CLI. А тест на UI в Cypress будет проверять только появление сообщения о создании домена.

В результате мы избавимся от дублирования тестовых сценариев, сократим нагрузку на сервера с Selenium и в перспективе совсем от них избавимся, когда тестирование UI будет делать только Cypress.

Заключение

Cypress подкупает быстрым стартом к написанию тестов и удобными средствами для отладки. При этом не стоит забывать, что скорость и стабильность тестов зависят от того, как они написаны: легковесные фикстуры и подготовленные ответы бэкенда дают большое ускорение, а сброс состояния позволяет избежать случайного влияния тестов друг на друга.

Cypress позволяет организовать как полноценные e2e тесты с прохождением пользовательских сценариев на реальном продукте, так и интеграционное тестирование отдельных компонентов фронтенда. Лучше заранее выбрать цель и договориться о правилах игры для всей команды.

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

Подробнее..

Jenkins Pipeline. Что это и как использовать в тестировании

08.02.2021 16:12:50 | Автор: admin

Меня зовут Александр Михайлов, я работаю в команде интеграционного тестирования компании ЮMoney.

Наша команда занимается приемочным тестированием. Оно включает в себя прогон и разбор автотестов на критичные бизнес-процессы в тестовой среде, приближенной по конфигурации к продакшену. Еще мы пишем фреймворк, заглушки, сервисы для тестирования в целом, создаем экосистему для автоматизации тестирования и обучаем ручных тестировщиков автоматизации.

Надеюсь, что эта статья будет интересна как новичкам, так и тем, кто съел собаку в автоматизации тестирования. Мы рассмотрим базовый синтаксис Jenkins Pipeline, разберемся, как создать джобу на основе пайплайна, а также я расскажу про опыт внедрения неочевидной функциональности в CI запуска и дожатия автотестов по условию.

Запуск автотестов на Jenkins инструкция

Не новость, что автотесты эффективнее всего проводить после каждого изменения системы. Запускать их можно локально, но мы рекомендуем делать это только при отладке автотестов. Больший профит автотесты принесут при запуске на CI. В качестве CI-сервера у нас в компании используется Jenkins, в качестве тестового фреймворка JUnit, а для отчетов Allure Report.

Чтобы запускать тесты на Jenkins, нужно создать и сконфигурировать джобу.

Для этого достаточно выполнить несколько несложных шагов.

1) Нажать Создать, выбрать задачу со свободной конфигурацией и назвать ее, например, TestJob.

Естественно, для этого у вас должны быть права в Jenkins. Если их нет, нужно обратиться к администратору Jenkins.

2) Указать репозиторий, откуда будет выкачиваться код проекта: URL, credentials и branch, с которого все будет собираться.

3) Добавить нужные параметры, в этом примере количество потоков (threadsCount) и список тестов для запуска (testList).

Значение *Test для JUnit означает Запустить все тесты.

4) Добавить команду для запуска тестов.

Наш вариант запускается на Gradle: мы указываем таску теста и передаем параметры в тесты.

./gradlew test -PthreadsCount=$threadsCount -PtestList=$testList

Можно выполнить шаг сборки Выполнить команду shell, либо через Gradle Plugin использовать шаг Invoke Gradle Script.

5) Нужно добавить Allure-report (должен быть установлен https://plugins.jenkins.io/allure-jenkins-plugin/) в Послесборочные операции, указав путь к артефактам Allure после прогона (по умолчанию allure-result).

Создав джобу и прогнав с ее помощью тестов, мы можем получить такой результат.

На скрине реальный прогон. По таймлайну видно, как тесты разделяются по потокам и где были падения.

Несложно заметить, что тесты у нас падают.

Почему падают тесты

Падения могут случаться по разным причинам. В нашем случае на это влияют:

  • ограниченные ресурсы тестового стенда,

  • большое число микросервисов (~140); если при запуске интеграционных тестов какой-то один микросервис подтормаживает, тесты начинают валиться,

  • большое число интеграционных тестов (>3000 E2E),

  • врожденная нестабильность UI-тестов.

В итоге проблемы по этим причинам мультиплицируются, и мы получаем комбинаторный взрыв, проявляющийся ошибками в прогонах тестов. Что делать? Дожимать.

Что такое дожим

Дожим это перезапуск автотестов в рамках их одного прогона. При успешном прохождении автотеста можно считать его пройденным, не учитывая предыдущие падения в прогоне.

Дожимать? Опасно же!

Возразите вы и будете полностью правы. Безусловно, дожим автотестов может спровоцировать пропуск дефекта на продакшн. Баг может быть плавающим при повторном запуске успешно просочиться и попасть на продакшн.

Но мы проанализировали статистику падений автотестов за длительный период и увидели, что большинство падений было связано с нестабильной тестовой средой, тестовыми данными. Поэтому мы взяли на себя риск и стали дожимать тесты. Прошло уже больше года, и не было ни одного дефекта, пропущенного на продакшн по этой причине. Зато стабильность тестов повысилась ощутимо.

Как решать задачу с дожимами

Мы пробовали разные решения: использовали модификацию поведения JUnit 4, JUnit 5, писали обертки на Kotlin. И, к сожалению, каждый раз реализация завязывалась на фичах языка или фреймворка.

Если процесс запускался с помощью JUnit 4 или JUnit 5, возможность перезапустить тесты была только сразу при падении. Тест упал, перезапустили его несколько раз подряд и если сбоил какой-то микросервис из-за нагрузки, либо настройки тестовой среды были некорректные, то тест все три раза падал.

Это сильно удлиняло прогон, особенно когда проблема была в настройке тестовой среды, что приводило к провалу множества тестов.

Мы взглянули на проблему шире, решили убрать зависимость от тестового фреймворка или языка и реализовали перезапуск на более высоком уровне на уровне CI. И сделали это с помощью Jenkins Pipeline.

Для этого подходил следующий алгоритм: получаем список упавших тестов и проверяем условия перезапуска. Если упавших тестов нет, завершаем прогон. Если есть, решаем, запускать их повторно или нет. Например, можно реализовать логику, чтобы не перезапускались тесты, если их упало больше какого-то критического числа. Или перед запуском можно сделать проверку доступности тестируемого сервиса.

Что такое Jenkins Pipeline

Jenkins Pipeline набор плагинов, позволяющий определить жизненный цикл сборки и доставки приложения как код. Он представляет собой Groovy-скрипт с использованием Jenkins Pipeline DSL и хранится стандартно в системе контроля версий.

Существует два способа описания пайплайнов скриптовый и декларативный.

1. Scripted:

node {stage('Example') {try {sh 'exit 1'}catch (exc) { throw exc}}}

2. Declarative

pipeline {agent anystages {stage("Stage name") {steps {}}}}

Они оба имеют структуру, но в скриптовом она вольная достаточно указать, на каком слейве запускаться (node), и стадию сборки (stage), а также написать Groovy-код для запуска атомарных степов.

Декларативный пайплайн определен более жестко, и, соответственно, его структура читается лучше.

Рассмотрим подробнее декларативный пайплайн.

  1. В структуре должна быть определена директива pipeline.

  2. Также нужно определить, на каком агенте (agent) будет запущена сборка.

  3. Дальше идет определение stages, которые будут содержаться в пайплайне, и обязательно должен быть конкретный стейдж с названием stage(name). Если имени нет, тест упадет в runtime с ошибкой Добавьте имя стейджа.

  4. Обязательно должна быть директива steps, в которой уже содержатся атомарные шаги сборки. Например, вы можете вывести в консоль Hello.

pipeline { // определение декларативного pipelineagent any // определяет, на каком агенте будет запущена сборкаstages { // содержит стейджи сборкиstage("Stage name") { // отдельный стейдж сборкиsteps { // набор шагов в рамках стейджаecho "Hello work" // один из шагов сборки}}}}

Мне нравится декларативный вид пайплайна тем, что он позволяет определить действия после каждого стейджа или, например, после всей сборки. Я рекомендую использовать его при описании пайплайнов на верхнем уровне.

pipeline {stages {stage("Post stage") {post { // определяет действия по завершении стейджаsuccess { // триггером исполнения секции является состояние сборки archiveArtifacts artifacts: '**/target/*'}}}}post { // после всей сборкиcleanup {cleanWs()}}}

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

При написании своего первого пайплайна я, естественно, использовал официальный мануал там подробное описание синтаксиса и степов.

Сначала я даже растерялся и не знал, с чего начать настолько там много информации. Если вы первый раз сталкиваетесь с написанием пайплайна, начать лучше со знакомства с генераторами фрагментов пайплайна из UI-интерфейса.

Если к URL вашего веб-интерфейса Jenkins добавить ендпойнт /pipelines-syntax, откроется страница, в которой есть ссылки на документацию и два сниппет-генератора, позволяющие генерировать пайплайн даже без знания его синтаксиса:

  • Declarative sections generator

  • Snippet Generator

Генераторы фрагментов помощники в мире Jenkins

Для создания пайплайна сначала нужно декларативно его описать, а затем наполнить степами. Давайте посмотрим, как вспомогательные инструменты нам в этом помогут.

  • Declarative sections generator (JENKINS-URL/directive-generator) генератор фрагментов для декларативного описания пайплайна.

Для добавления стадии нужно написать ее имя и указать, что будет внутри (steps). После нажатия кнопки Сгенерировать будет выведен код, который можно добавлять в пайплайн.

stage(start tests){steps{ //One or more steps needs to be included within the steps block}}

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

  • В Sample Step выбрать build: Build a job.

  • (Дальше функционал подсказывает) необходимо определить параметры, которые будут переданы в джобу (для примера задано branch, project).

  • Нажать кнопку Generate в результате сформируется готовый рабочий код.

Изменим параметры джобы на те, которые определили при ее создании.

build job QA/TestJob, parameters: [                        string(name: 'threadsCount', value: 16),                         string(name: 'testList', value: *Test),                        string(name: 'runId', value: runId)]

где threadsCount - кол-во потоков для распараллеливания тестов, testList - список тестов для запуска, runId - идентификатор прогона тестов. Для чего нужны эти параметры, расскажу далее.

Snippet Generator удобен тем, что подсвечивает шаги в зависимости от установленных плагинов. Если у вас есть, например, Allure-report, то плагин сразу покажет наличие расширения и поможет сгенерировать код, который будет работать.

Вставим сгенерированный код степа в пайплайн на следующем шаге.

Запуск тестов с помощью Pipeline инструкция

Итак, давайте с помощью Declarative sections generator создадим пайплайн. В нем нужно указать директивы: pipeline, agent (агент, на котором будет запускаться пайплайн), а также stages и steps (вставка ранее сгенерированного кода).

Так получится пайплайн, который запустит джобу, созданную на предыдущем шаге через UI.

pipeline {agent {label any}stages {stage("start test") {steps{build job: '/QA/TestJob',parameters: [string(name: 'threadsCount', value: threadsCount),string(name: 'runId', value: runId),string(name: 'testList', value: testList)]}}} }

Напомню, что в параметры для запуска тестов мы передавали количество потоков и список тестов. Теперь к этому добавляем параметр runId (идентификатор прогона тестов) он понадобится позднее для перезапуска конкретного сьюта тестов.

Чтобы запустить пайплайн, нужно создать проект.

  1. New Item -> Pipeline.

Для этого нужно нажать на кнопку Создать проект и выбрать не джобу со свободной конфигурацией, а джобу Pipeline осталось указать ей имя.

  1. Добавить параметры runId, threadsCount, testList.

  1. Склонировать из Git.

Пайплайн можно описать непосредственно как код и вставить в поле, но для версионирования нужно затягивать пайплайн из Git. Обязательно указываем путь до скрипта с пайплайном.

Готово, джобу можно запускать.

Хотим добавить немного дожатий

На этом этапе у нас уже готова джоба для запуска проекта с автотестами. Но хочу напомнить, что наша задача не просто запускать тесты, а добавить им стабильности, исключив падения из-за внешних факторов. Для достижения этого результата было принято решение дожать автотесты.

Для реализации нужно:

  1. вынести шаг запуска тестов в библиотечную функцию (shared steps),

  2. получить упавшие тесты из прогона,

  3. добавить условия перезапуска.

Теперь немного подробнее про каждый из этих шагов.

Многократное использование шагов Shared Steps

В процессе написания пайплайнов я столкнулся с проблемой постоянного дублирования кода (часто используемых степов). Этого хотелось избежать.

Решение нашлось не сразу. Оказывается, для многократного использования кода в Jenkins есть встроенный механизм shared libraries, который позволяет описать методы один раз и затем применять их во всех пайплайнах.

Существуют два варианта подключения этой библиотеки.

  1. Написанный проект/код подключить через UI Jenkins. Для этого требуются отдельные права на добавление shared libraries или привлечение девопс-специалистов (что не всегда удобно).

  2. Хранить код в отдельном проекте или в проекте с пайплайнами. При использовании этот код подключается как динамическая библиотека и выкачивается каждый раз при запуске пайплайна.

Мы используем второй вариант размещаем shared steps в проекте с пайплайнами.

Для этого в проекте нужно:

  • создать папку var,

  • в ней создать файл с названием метода, который планируется запускать например, gradlew.groovy,

  • стандартно определить имя метода (должен называться call), то есть написать def call и определить входящие параметры,

  • в теле метода можно написать произвольный Groovy-код и/или Pipeline-степы.

Pipeline script:

//Подключение библиотеки//https://www.jenkins.io/doc/book/pipeline/shared-libraries/ - описание с картинкамиlibrary identifier: 'pipeline-shared-lib'  pipeline {stages {stage("Build") {steps {gradlew(tasks: ["build"]) // вызов метода из библиотеки}}}}

var/gradlew.groovy

def call(Map<String, List<String>> parameters) {  // стандратное имя для глобального методаdef tasks = parameters["tasks"]def args = parameters["args"] ?: []sh "./gradlew ${args.join(' ')}     ${tasks.join(' ')}"    // произвольный groovy код + pipeline-методы}

Вынесение запуска тестов в shared steps в /var

  1. Выносим startTests.groovy в /var.

Во-первых, нужно вынести запуск тестов в отдельный метод. Выглядит это так создаем файл, называем метод def call, берем кусок кода, который был в пайплайне, и выносим его в этот step.

def call(Map<String, String> params) {    def threadsCount = params["threadsCount"] ?: "3"    def testList = params["testList"] ?: "*Test"    stage("start test job") {      runTest = build job: '/QA/TestJob',                  parameters: [                         string(name: 'threadsCount', value: threadsCount),                         string(name: 'runId', value: runId),                         string(name: 'testList', value: testList)],                         propagate: false   }}

Для передачи параметров используется Map<String, String>. Почему не передавать каждый параметр отдельно? Это не очень удобно, т.к. в Groovy параметры не обозначены по названиям. При использовании Map синтаксис позволяет указать key:value через двоеточие. В коде (в месте вызова метода) это отображается наглядно.

Структура проекта будет выглядеть так.

  1. Подключение shared steps как внешней библиотеки.

Дальше нужно добавить вынесенные шаги в пайплайн. При динамической подгрузке библиотек (во время запуска пайплайна) эти шаги выкачиваются из репозитория и подключаются на лету.

Сделать это можно с помощью сниппет-генератора выбираем степ library и указываем ветку, в которую все будет собираться и репозиторий. Дальше нажимаем кнопку Сгенерировать и вставляем получившийся пайплайн.

library changelog: false,    identifier: 'shared-lib@master',    retriever: modernSCM([   $class    : 'GitSCMSource',   remote   : 'ssh://git@bitbucket.ru/qa/jenkins-groovy-scripts.git'])

Теперь после подключения shared steps вместо шага запуска тестов build нужно вставить startTest. Не забудьте, что имя метода должно совпадать с именем файла.

Теперь наш пайплайн выглядит так.

//Динамическое подключение библиотекиlibrary changelog: false,    identifier: 'shared-lib@master',    retriever: modernSCM([   $class   : 'GitSCMSource',  remote   : 'ssh://git@bitbucket.ru/qa/jenkins-groovy-scripts.git'])pipeline {   agent {  label any  }  stages { stage("start test") {    steps{   startTests(runId: runId ) //Вызов метода из библиотеки  }    }  }}

Первый шаг реализован, теперь можно многократно запускать тесты в разных местах. Переходим к 2 шагу.

Получение упавших тестов из прогона

Теперь нам нужны упавшие тесты. Каким образом их извлечь?

  • Установить в Jenkins плагин JUnit Test Result Report и использовать его API.

  • Взять результаты прогона JUnit (обычно в формате XML), распарсить и извлечь нужные данные.

  • Запросить список упавших тестов из нужного места.

В нашем случае таким местом является собственный написанный сервис Reporter. Во время прогона тестов результат по каждому из них отправляется именно туда. В конце прогона делаем http-запрос и получаем упавшие тесты.

http://reporter:8080/failedTests/$runId

Добавление условий перезапуска

На этом шаге следует добавить getFailedTests.groovy в /var. Представим, что у вас есть такой сервис Reporter. Нужно назвать файл getFailedTests, сделать запрос httpRequest в этот сервис и распарсить его.

def call(String runId) {    def response = httpRequest httpMode: 'GET',     url: "http://reporter:8080/failedTests/$runId"     def json = new JsonSlurper().parseText(response.content)        return json.data}

Отлично, второй шаг выполнен. Осталось добавить ту самую изюминку условия перезапуска. Но сначала посмотрим, что получается.

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

Условия перезапуска

Какие условия для перезапуска можно реализовать?

Приведу часть условий, которые используем мы.

1) Если нет упавших тестов, прогон завершается.

if (countFailedTests == 0) {echo FINISHED   }

2) Как я уже писал выше, на тестовой среде ресурсы ограничены, и бывает такое, что ТС захлебывается в большом количестве параллельных тестов. Чтобы на дожатии избежать падений тестов по этой причине, понижаем число потоков на повторном запуске. Именно для этого при создании джобы и в самом пайплайне мы добавили параметр threadsCount.

Если и после уменьшения потоков тесты не дожимаются (количество упавших тестов на предыдущем прогоне равно числу упавших тестов на текущем), тогда считаем, что дело не во влиянии ТС на тесты. Останавливаем прогон для анализа причин падений и тем самым предотвращаем последующий холостой запуск.

if (countFailedTests == previousCountFailedTests) { echo TERMINATED - no one new passed test after retry}

3) Третья и самая простая проверка состоит в том, что если падает большое количество тестов, то дожимать долго. Скорее всего, причина падений какая-то глобальная, и ее нужно изучать.

Для себя мы определили: если тестов > 40, дожимать не автоматически не будем, потому что 40 наших E2E могут проходить порядка 15 минут.

if (countFailedTests > FAILEDTESTSTRESHOLD) {   echo TERMINATED - too much failed tests   }
Получился метод:

https://github.com/useriq/retry-flaky-tests/blob/master/jenkins-pipeline-retry/var/testsWithRerun.groovy

def call(Map<String, String> params) {    assert params["runId"]    def threadsCount = params["threadsCount"] ?: "8"    def testList = params["testList"] ?: "*Test"    def runId = params["runId"]    int FAILED_TESTS_TRESHOLD = 40    def countFailedTests = 0    def failedTests    int run = 1    boolean isFinished = false    int threads = threadsCount as int    while (run <= Integer.valueOf(runCount) && !isFinished) {        if (run == 1) {            startTests()        } else {            if (countFailedTests > 0) {                threads = reduceThreads(threads)                testList = failedTests.toString().minus('[').minus(']').minus(' ')                startTests()            }        }        stage("check ${run}_run result ") {            failedTests = getFailedTests(runId)            def previousCountFailedTests = countFailedTests            countFailedTests = failedTests.size()            if (countFailedTests == 0) {                echo "FINISHED"                isFinished = true            }            if (countFailedTests > FAILED_TESTS_TRESHOLD) {                echo "TERMINATED - too much failed tests > ${FAILED_TESTS_TRESHOLD}"                isFinished = true            }            if (countFailedTests == previousCountFailedTests) {                echo "TERMINATED - no one new passed test after retry"                isFinished = true            }        }        run += 1    }}

Последние два условия так называемые fail fast. Они позволяют при глобальных проблемах на тестовом стенде не делать прогоны, которые не приведут к дожиму тестов, но будут занимать ресурсы тестового стенда.

Итоговый pipeline

Итак, все 3 шага реализованы итоговый пайплайн выглядит так.

library changelog: false,       identifier: 'shared-lib@master',       retriever: modernSCM([               $class       : 'GitSCMSource',               remote       : 'ssh://git@bitbucket.ru/qa/jenkins-groovy-scripts.git']) assert runId != nullpipeline {   agent {       label any   }   stages {       stage("start test") {           steps {             testsWithRerun(runId: runId)           }       }   }}

Визуализация с Blue Ocean

Как все это выглядит при прогоне в Jenkins? У нас, к примеру, для визуализации в Jenkins установлен плагин Blue Ocean.

На картинке ниже можно увидеть, что:

  1. запустился метод testwith_rerun,

  2. прошел первый запуск,

  3. прошла проверка упавших тестов,

  4. запустился второй прогон,

  5. после успешной проверки джоба завершилась.

Вот так выглядит визуализация нашего настоящего прогона.

В реальном примере две ветки: мы параллельно запускаем два проекта с тестами (на Java и Kotlin). Можно заметить, что тесты одного проекта прошли с первого раза, а вот тесты другого пришлось дожимать еще раз. Таким образом визуализация помогает найти этап, на котором падают тесты.

А так выглядит реальный timeline приемки релиза.

После первого прогона отправили дожимать упавшие тесты. Во второй раз упавших тестов намного меньше, дожимаем в третий раз и вуаля, успешный build.

Задача решена.

Итог

Мы перенесли логику перезапусков упавших тестов из тестового проекта на уровень выше на CI. Таким образом сделали механизм перезапуска универсальным, более гибким и независимым от стека, на котором написаны автотесты.

Раньше наши тесты дожимались безусловно, по несколько раз, с неизменным количеством параллельных потоков. При перегрузке тестового стенда, некорректных настройках тестового окружения либо каких-то других проблемах красные тесты перезапускались фиксированное число раз без шансов дожаться. В худшем случае прогоны могли длиться часами. Добавив условия fail fast, мы сократили это время.

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

Какой профит мы получили:

  • уменьшили time-to-market тестируемых изменений,

  • сократили длительность аренды тестового стенда под приемочное тестирование,

  • увеличили пропускную способность очереди приемочного тестирования,

  • не завязаны на тестовый фреймворк (под капотом может быть что угодно дожатия будут работать),

  • поделились знаниями об использовании Jenkins Pipeline.

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

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru