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

Integration testing

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, поэтому стоит заранее оценить количество параллельных запусков и спланировать масштабирование инфраструктуры.

Подробнее..

Система под контролем как автоматизировать интеграционные тесты

29.10.2020 16:15:50 | Автор: admin

Привет! Меня зовут Ксения Якиль. Я пишу core-сервисы на C и Go в бэкенд-отделе Badoo и Bumble. Наш бэкенд это высоконагруженная распределённая система, обслуживающая пользователей по всему миру. Она оперирует большими массивами данных и делает всю ту магию, благодаря которой люди находят друг друга.

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

Знакомьтесь, сервис М!

В нашем ведении находился один из очень важных сервисов M. Он был первоисточником информации о пользователях и их симпатиях друг к другу. М был самодостаточным, большим, надёжным и даже был покрыт юнит- и функциональными тестами.

Сервис состоит из фронта (Front) и нескольких шардов (S1SN):

Но с увеличением количества задач в одиночку М перестал справляться так хорошо, как раньше. Поэтому у него появились товарищи другие сервисы: мы выделили отдельные логические части M и обернули их в сервисы на Go (Search и Supervisor), добавили Kafka и Consul.

И стало так:

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

  • Работает ли функционал, в котором участвуют несколько сервисов?

  • Поднимается ли система в заданной конфигурации?

  • Что будет, если один из сервисов вернёт некорректный ответ?

  • Что сделает наша система, если один из сервисов будет недоступен: вернёт ожидаемые ошибки, повторит отправку, выберет другой инстанс и отправит запрос туда или вернет закешированные данные?

Мы знаем, как должна вести себя система. Но совпадают ли ожидания и реальность?

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

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

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

Мы понимали, что наряду со всеми очевидными плюсами у интеграционных тестов есть не менее очевидные минусы: увеличение времени прогона тестов, нестабильность, сложность написания. Но этого было недостаточно, чтобы остановить нас.

Требования к фреймворку

Какие требования мы предъявляли к интеграционному фреймворку?

  • Легковесность: минимум абстракций и простота добавления новых тестов.

  • Обозримое (по возможности небольшое) время прохождения тестов. Требовались быстро поднять инфраструктуру и осуществить прогон тестов.

  • Запуск разных конфигураций системы. Фреймворк должен позволять настраивать каждый сервис, запускать разные наборы сервисов (подсистемы) и на них отдельно прогонять тесты. Путь от простого к сложному: сначала убеждаемся, что работает небольшая подсистема, потом усложняем её, проверяем и так далее.

  • Реализация на языке Go, поскольку наш отдел разработки пишет на нём. Нам Go очень нравится, а тестировщики быстро его освоили и тоже пишут на нем фреймворк и интеграционные тесты.

С высоты МКС (схематичный план)

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

Модули нашего фреймворка предоставляют всё необходимое для генерации данных, ожидания выполнения запросов, проверки ответов, работы с инфраструктурой и многое другое. Поскольку фреймворк написан на Go, мы решили использовать go testing, а сами тесты поместили в отдельные файлы.

Для настройки и очистки окружения мы используем модуль Testify. Он позволяет создать suite, в котором определены функции:

  • SetupSuite. Вызывается до прохождения всех тестов для данного suite. Именно здесь мы будем осуществлять подготовку окружения.

  • TearDownSuite. Вызывается после прохождения всех тестов для suite. Тут мы почистим за собой инфраструктуру.

  • SetupTest. Вызывается перед каждым тестом для suite. Здесь мы можем осуществлять какую-то локальную подготовку к тесту.

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

Собираем инфраструктуру

Инфраструктура должна предоставлять много возможностей:

  1. Настраивать разную конфигурацию наших сервисов.

  2. Поднимать сторонние сервисы (наподобие Kafka и Consul). Если использовать инстансы внешних сервисов на devel, то проведение интеграционного тестирования может влиять на его состояние. Это приведёт к нестабильному и неожиданному для наших коллег поведению системы. Кроме того, на результаты наших интеграционных тестов смогут влиять действия других отделов придётся тратить больше времени на расследование падений. Повысить стабильность и воспроизводимость тестов можно с помощью изоляции двух сред. Поэтому мы хотели использовать отдельные запущенные инстансы в своей тестовой среде. В качестве бонуса это даёт возможность использовать сервисы любой версии и конфигурации, быстрее проверять гипотезы и не согласовывать изменения с другими отделами.

  3. Работать с этой инфраструктурой: остановить Kafka/Consul/свои сервисы, исключить их из сети или включить в сеть. Нужна большая вариативность.

  4. Запускать на разных машинах, например на машинах разработчиков, QA-инженеров и CI.

  5. Воспроизводить падения тестов. Если тестировщик увидел на своей машине, что тест не прошёл, разработчик должен с минимальными усилиями получить эту ошибку на своей машине. Мы хотели избежать различий в библиотеках и зависимостях на разных машинах (в том числе и на серверах для CI).

Мы решили использовать Docker и обернули сервисы в контейнеры: тесты будут создавать свою сеть (Docker network) для каждого прогона и включать контейнеры в неё. Это хорошая изоляция для тестов из коробки.

Запуск в контейнере

Во фреймворке мы запускаем сервис в контейнере с помощью модуля testcontainers-go, который по факту представляет собой прослойку между Docker и нашими тестами на Go.

Отправляем запрос в этот модуль с характеристикой нашего сервиса. Получаем структуру контейнера и полный спектр возможностей: запускать и останавливать контейнер, узнавать его статус, включать в сеть, исключать из сети и так далее. Вся эта реализация под капотом у testcontainers-go.

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

Рабочее окружение

Недостаточно просто поднять сервис в контейнере. Нужно подготовить для него тестовое окружение.

  • Создаём иерархию каталогов на хосте.

  • Копируем все необходимые данные для нашего сервиса (скрипты, файлы, снепшоты и т.д.) в соответствующие директории.

  • Создаём дефолтный файл конфигурации и тоже помещаем его в эту иерархию.

  • Монтируем корень этой иерархии на хосте в Docker-контейнер.

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

Конфигурация

Здесь мы использовали простое решение.

Через Entrypoint задаём переменные окружения, аргументы запуска и подготовленный файл конфигурации. Когда контейнер поднимется, он выполнит всё, что указано в Entrypoint.

После этого сервис можно считать сконфигурированным. Пример:

Адрес сервиса

Итак, сервис поднялся в контейнере. У него есть рабочее окружение и определённая конфигурация для теста. Как найти другие сервисы?

Внутри Docker network всё просто.

  • При создании контейнера мы генерируем ему уникальное имя и к этому имени обращаемся как к адресу: используем имя контейнера как hostname.

  • Порты мы знаем заранее, так как на предыдущем этапе подготавливали файлы конфигурации и указывали порты для наших сервисов.

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

Если тесты запускаются на локальной машине, они не могут обращаться к сервису по имени его контейнера, так как адресация по имени контейнера внутри Docker network это абстракция самого Docker. Нам нужно найти номер порта на локальном хосте, который соответствует порту сервиса в Docker network. После поднятия контейнера мы получим соответствие внутреннего порта сервиса (inner port) порту на локальном хосте (external port). Последний и будем использовать в тестах.

Внешние сервисы

Наверняка в вашей инфраструктуре присутствуют сторонние сервисы, например базы данных и service discovery. Их конфигурация в идеале должна совпадать с той, что на продакшене. Если сервис простой (например, Consul в конфигурации одного процесса) мы его тоже можем запустить с помощью testcontainers-go. Но если сервис многокомпонентный (например, Kafka из нескольких брокеров, где требуется ZooKeeper), то можно не страдать и использовать для этого Docker Compose.

Как правило, в процессе интеграционного тестирования не требуется обширный доступ к работе с внешними сервисами, так что Docker Compose хорошо подходит для наших целей.

Фаза загрузки

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

Что делать?

  1. Самое простое решение использовать sleep. После запуска контейнера ждём некоторое время. Как только оно прошло, считаем, что сервисы готовы к работе. Это плохое решение, поскольку запуск тестов происходит на разных машинах, а скорость загрузки сервисов может изменяться как в большую, так и в меньшую сторону.

  2. Открывать порты сервиса по мере готовности. Как только сервис прошёл фазу загрузки и готов принимать запросы клиентов, он открывает порты. Для тестового окружения это знак разрешения на запуск тестов. Однако есть нюанс: при создании контейнера Docker сразу открывает external port для сервиса, даже если последний ещё не начал слушать соответствующий internal port в контейнере. Поэтому в тестах сразу будет установлено соединение и попытка чтения из соединения приведёт к EOF. Когда сервис откроет internal port, тестовый фреймворк сможет отправить запрос. Только после этого мы будем считать, что сервис готов к работе.

  3. Запрашивать статус сервиса. Cервис сразу открывает порты, на запрос статуса отвечает Готов, если уже загрузился, и Не готов, если нет. В тестах мы будем периодически спрашивать сервис о его статусе и, как только получим ответ Готов, перейдём к фазе тестирования.

  4. Регистрировать в стороннем сервисе или базе данных. Мы регистрируем сервисы в Consul. Можно использовать:

    1. Факт появления сервиса в Consul как сигнал о готовности. Состояние сервиса можно отслеживать с помощью блокирующего запроса с тайм-аутом. Как только сервис зарегистрируется, Consul пришлет ответ на запрос с информацией об изменении статуса сервиса.

    2. Анализ состояния сервиса с помощью проверки его checka. Фреймворк для интеграционного тестирования получает информацию о новом сервисе из пункта 1 и начинает отслеживать изменения его статуса. Когда статусы всех сервисов будут passing, считаем, что они готовы к работе.

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

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

Поднятие всех сервисов

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

В какой последовательности осуществлять запуск? Идеальный вариант не иметь строгой последовательности. Это позволяет запускать сервисы параллельно и значительно сократить время создания инфраструктуры (время запуска контейнера + время загрузки сервиса). Чем меньше связей, тем проще добавлять новый сервис в инфраструктуру.

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

Инфраструктура во время тестирования

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

Изменение конфигурации сервиса

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

Добавление нового сервиса

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

Работа с сетью

Включение контейнеров в сеть и их исключение из неё, приостановка (pause/unpause) работы контейнеров, iptables позволяют нам эмулировать сетевые ошибки и проверять реакцию системы на них.

Инфраструктура после тестирования

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

  • Если было изменение конфигурации сервиса, делаем откат на предыдущую (дефолтную) конфигурацию.

  • Если было добавление нового сервиса, удаляем его.

  • Если были любые изменения в сети (iptables, приостановка контейнеров и т. д.), отменяем их.

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

После завершения test suite работа всех сервисов в инфраструктуре тоже завершается, контейнеры убиваются, тестовая сеть опускается. Если test suite не успел завершиться по истечении тайм-аута или в результате ошибки, действуем точно так же. Только в случае явного указания фреймворку оставить контейнеры после прогона (например, для отладки), инфраструктура остаётся.

Ускорение тестов

Ждать вечность, пока пройдут интеграционные тесты, как правило, никому не хочется. Хотя в это время можно выпить кофе и сделать ещё много чего интересного.

Что мы можем сделать для ускорения тестов?

  • Группировать read-only-тесты и запускать их параллельно в рамках одного теста (в Go при помощи горутин это делается максимально просто). Эти тесты должны работать на изолированном множестве данных.

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

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

  • Запускать несколько тестовых инфраструктур параллельно (если позволяют ресурсы). По сути, это параллельный прогон test suite.

  • Переиспользовать контейнеры.

  • Задаться вопросом, действительно ли в контейнере необходим новый сервис, или достаточно мока. Под моками я имею в виду не интерфейсные моки, которые мы используем в юнит-тестировании, а отдельные серверы. Мок прикидывается одним из наших сервисов и умеет работать по его протоколу. Для остальных поднятых сервисов в текущей тестовой инфраструктуре он неотличим от оригинального. Мок позволяет задать нужную нам логику поведения реального сервиса и не поднимать его в контейнере.

В тестах мы запускаем мок на определённом адресе. Этот адрес уже поднятые сервисы в текущей инфраструктуре узнают через конфиг или service discovery (Consul в нашем случае) и могут отправлять на него запросы.

Мок получает запрос и вызывает handler, который мы указали. На Go в тесте это выглядит примерно так:

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

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

Мы используем моки для всех наших сервисов это помогает выиграть много времени при тестировании.

Реализация

Наш фреймворк располагается в том же репозитории, что и тестируемые сервисы, лежит в отдельной директории autotests. Внутри неё есть несколько модулей:

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

Mock содержит реализацию мока-сервера для каждого нестороннего сервиса.

В suite находится общая реализация. Он умеет работать с сервисами, ожидать их загрузки, проверять работоспособность и многое другое.Environment хранит информацию о текущем тестовом окружении (какие сервисы запущены), отвечает за сеть.

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

Помимо модулей самого фреймворка, на момент написания статьи у нас были созданы 21 test suites, в том числе и smoke test suite. Каждый создаёт свою инфраструктуру с необходимым набором сервисов. Тесты находятся в файлах внутри test suite.

Запуск конкретного test suite выглядит примерно так:

go test -count=1 -race -v ./testsuite $(TESTSFLAGS) -autotests.timeout=15m

Поскольку сервисы коллег из других отделов мы хотели тоже перевести на наш фреймворк, core-функционал фреймворка был вынесен в общий core-репозиторий.

Отладка

Ура! Мы научились подготавливать инфраструктуру и прогонять тесты. Приятно видеть зелёный результат интеграции. Но так бывает не всегда, поэтому начинаем разбираться с падениями. Первое, что приходит на ум, изучить логи сервисов.

Предположим, suite содержит огромное количеством тестов, и один из них не получил от сервиса ожидаемого ответа. Где-то в недрах лога сервиса нам нужно найти кусочек, который соответствует времени прохождения упавшего теста. В этом помогает простой и удобный инструмент маркеры.

  • Добавляем в сервис команду log_notice, при получении которой он записывает в свой лог сообщение из запроса.

  • Перед началом теста отправляем log_notice с названием теста во все поднятые сервисы. По окончании теста делаем то же самое.

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

Как быть, если сервис не смог подняться и не успел сделать запись в лог? Скорее всего, он записал в stderr/stdout дополнительную информацию. Команда docker logs позволяет получать данные из стандартных потоков ввода-вывода это поможет нам понять, что случилось.

А теперь предположим, что данных из логов не достаточно для локализации ошибки. Время обратиться к более серьёзным методам!

Указываем в конфигурации фреймворка необходимость оставлять инфраструктуру после прогона всех тестов в suite. Благодаря этому мы получаем полный доступ к системе. Можно узнать статус сервиса, получить данные из него, отправлять различные запросы, анализировать файлы сервиса на диске, а так же использовать gdb/strace/tcpdump и профилирование. Дальше мы строим гипотезу, пересобираем образ, запускаем тесты и итеративно находим корень проблемы.

Чтобы отладка не превращалась в мучительный отлов багов, тесты должны быть максимально воспроизводимыми. Например, если данные генерируются с помощью random, в случае ошибки нужно выводить информацию о seed и/или о том, какие данные были запрошены.

QA

Как с интеграционным фреймворком работает тестировщик? Ему не нужно самостоятельно вручную поднимать все сервисы. Интеграционные тесты делают это за него и помогают создать нужную инфраструктуру. Если suite на запланированную инфраструктуру ещё не написан, он быстро добавляет его сам.

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

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

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

CI

Оказалось, что встроить интеграционные тесты в CI очень просто. Мы используем TeamCity. Код фреймворка находится в репозитории с кодом сервисов. Сначала собираются сервисы, создаются образы, далее происходят сборка фреймворка и наконец его запуск.

Мы научили TeamCity понимать по выводу тестового фреймворка, какие тесты прошли, а какие нет. После окончания прогона отображается, сколько и каких тестов не прошло. Данные всех сервисов после прогона каждого suite сохраняются и публикуются в TeamCity в качестве артефактов для конкретной сборки и прогона.

Итоги

Ниже результаты проделанной работы.

  • Жить стало спокойнее. Меньше проблем с интеграцией просачивается на продакшен. Как следствие более стабильный прод.

  • Мы научились поднимать разную инфраструктуру и покрывать больше сценариев за меньшее время.

  • Мы работаем с инфраструктурой во время прохождения тестов. Это даёт больше возможностей для реализации разных тест-кейсов.

  • Мы ловим больше багов на этапе разработки. Positive-сценарии пишут сами разработчики, отлавливая часть ошибок и сразу их решая. Уменьшается round-trip бага.

  • Мы избавили тестировщиков от написания positive-кейсов. QA-инженеры могут сосредоточиться на более сложных сценариях.

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

  • Мы написали MVP фреймворка для интеграционного тестирования довольно быстро за пару недель. Задача оказалась не слишком трудоёмкой.

  • Мы используем фреймворк уже больше года.

В общем, фреймворк экономит наше время и даёт больше уверенности. Мы улучшаем его и расширяем область применения, добавляем интеграционные тесты для других сервисов компании.

Однако интеграционное тестирование имеет ряд минусов, которые стоит учитывать.

  • Увеличение времени прогона тестов. Системы сложные, запросы выполняются в нескольких сервисах.

  • Нестабильность, так как система состоит из асинхронных компонентов. С этим можно и нужно бороться. Мы тоже это делаем: доля нестабильных тестов у нас близка к нулю.

  • Сложность написания тестов. Нужно понимать, как работает система в целом, каково её ожидаемое поведение и как её можно сломать.

  • Возможность расхождения инфраструктуры в тестах и на продакшене. Если не все сервисы на проде в контейнерах, то тестовое окружение не на 100% совпадает с продакшеном. У нас как раз часть сервисов на проде не в контейнерах, но мы пока не сталкивались с проблемами из-за их тестирования в контейнерах.

И, наконец, главный вопрос, на который нужно ответить: необходим ли вам фреймворк для интеграционных тестов?

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

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

Успехов и удачи!

Подробнее..

QA Online Meetup 2411

19.11.2020 18:06:36 | Автор: admin
Присоединяйтесь на второй открытый митап 24 ноября, который посвятим интеграционному тестированию. Приготовили отличные доклады, и вот о чем поговорим: зачем и как использовать Cypress для интеграционного тестирования, и возможно ли добиться нуля ошибок по таким тестам?

Ждем вас онлайн!



О чем будем говорить


Cypress для интеграционного тестирования. Зачем? Как?

Светлана Голдобина, Райффайзенбанк

О спикере: Старший тестировщик в Райффайзенбанке, команда Cash Management. Опыт в автоматизации тестирования больше 3-х лет в крупных банках. За карьерный путь успела поработать как с BDD фреймворками, в том числе Akita, и поучаствовать в ее развитии, так и перейти полностью на сторону тестирования на стеке разработчиков JavaScript/TypeScript, Java + Spring.

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

О докладе: В нашей команде писали тесты в BDD стиле на русском языке (Selenium/Selenide + Cucumber + Java). Казалось бы, куда еще проще и прозрачнее для команды? Однако, как только мы лишились нескольких QA, и разработчикам пришлось писать и дорабатывать тесты, наш инструмент стал стоппером в тестировании, и BDD тут ничем не помог. В докладе расскажу, как мы опустили тестирование на дно и начали его восстанавливать.

Как мы добились нуля ошибок по итогам интеграционных тестов

Максим Плавченок, Bercut

О спикере: Работаю в компании Bercut с 2002. Начинал карьеру в телекоме: прошёл путь от сменного инженера до руководителя направления интеграционного тестирования. Был разработчиком, тестировщиком, менеджером продукта. Люблю заниматься тем, что приносит проблемы (и решать их), поэтому и пришёл в тестирование.

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

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

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

>>> Начнем митап в 18:00 (МСК).
Зарегистрируйтесь, чтобы получить ссылку: письмо с трансляцией придет на почту всем участникам.

До встречи онлайн!
Подробнее..

Категории

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

  • Имя: Макс
    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