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

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

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

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

Системный дизайн

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

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

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

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

Сервис состоял из Front и нескольких осколков (S1… SN):

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

Вот как это выглядело:

За короткое время простой сервис на основе C превратился в довольно сложную структуру из 5 компонентов и многих других экземпляров. В итоге мы закончили с распределенной системой и списком вопросов вверху:

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

Мы знали, как должна работать система, но были ли наши ожидания реалистичными?

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

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

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

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

Требования

Поскольку было принято решение разработать «совершенно новую» структуру, следующим шагом было определение ее требований. Мы составили следующий список:

  • Он должен быть легким, то есть с минимумом абстракций и легкостью добавления новых тестов.
  • Требуется измеримое время (где возможно, короткое) для завершения теста. Инфраструктура, необходимая для поддержки быстрого развертывания и тестирования.
  • Систему нужно было запускать в различных конфигурациях. Фреймворк должен был поддерживать конфигурацию каждой службы, запуск различных наборов служб (подсистем) и запуск тестов для каждого из них отдельно. Переходя от простого к сложному, после того, как мы убедились, что второстепенная подсистема работает должным образом, нам пришлось усложнить ее, снова протестировать и так далее.
  • Он должен был быть основан на Go, так как это был язык, который использовала наша команда разработчиков. Нам это очень нравится, и наши тестировщики освоили его в кратчайшие сроки и в настоящее время используют его для компиляции фреймворка и интеграционных тестов.
  • Это необходимо для запуска сторонних сервисов (например, Kafka и Consul). Если мы используем сторонние экземпляры в среде предпроизводственного этапа, интеграционное тестирование может повлиять на его статус. В результате система будет вести себя нестабильно, чего не ожидают наши коллеги. Кроме того, действия других отделов также повлияют на результаты наших интеграционных тестов, и нам потребуется больше времени, чтобы исследовать сбои. Стабильность и воспроизводимость тестов можно повысить, изолировав две среды, поэтому мы хотели использовать независимые запущенные экземпляры в нашей тестовой среде. В качестве бонуса нам стало проще использовать любые версии и конфигурации сервиса, быстрее проверять гипотезы и не соглашаться с модификациями других отделов.
  • Он должен был работать с этой инфраструктурой: останавливать Kafka / Consul / наши сервисы, исключать их из сети или добавлять их в сеть. Нам нужна была повышенная гибкость.
  • Его нужно было запускать на разных машинах, в том числе на машинах разработчиков, QA-инженера и CI.
  • Необходимо было воспроизвести сбой теста. Если тестировщик видит, что тест не прошел на своей машине, разработчик должен иметь возможность воспроизвести ошибку на своей машине с минимальными усилиями. Мы стремились избежать несоответствия в библиотеках и зависимостях на разных машинах (включая службы CI).

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

Постройте инфраструктуру

Дизайн-макет (с высоты птичьего полета)

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

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

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

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

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

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

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

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

Операционная среда

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

  • Во-первых, мы создаем иерархию каталогов на хосте.
  • Затем мы копируем все данные, которые нужны нашему сервису (скрипты, файлы, снимки и т. Д.) В соответствующие каталоги.
  • Затем мы создаем файл конфигурации по умолчанию и добавляем его в эту иерархию.
  • И, наконец, мы монтируем корень иерархии от хоста к контейнеру Docker.

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

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

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

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

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

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

Пример:

Адрес службы

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

В сети Docker это действительно просто.

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

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

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

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

В вашей инфраструктуре должны быть сторонние службы, например базы данных и обнаружение служб. В идеале их конфигурация должна соответствовать производственной. Простой сервис вроде Consul с однопроцессной конфигурацией можно запустить с помощью testcontainers-go. Однако в случае с многокомпонентным сервером, таким как Kafka, с несколькими брокерами и необходимостью ZooKeeper, не нужно мучиться - вы можете просто использовать Docker Compose.

Обычно интеграционное тестирование не требует обширного доступа при работе с внешними сервисами, и это делает Docker Compose удобным.

Этап загрузки

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

Что мы можем здесь сделать?

  1. Проще всего использовать сон. После запуска контейнера мы ждем некоторое время, и по его истечении мы думаем, что службы готовы к работе. Это не лучший метод, поскольку все тесты выполняются на разных машинах, и скорость загрузки службы может увеличиваться или уменьшаться.
  2. Откройте сервисные порты, когда будете готовы. Как только сервис проходит стадию загрузки и готов принимать запросы клиентов, он открывает порты. Для тестовой среды это сигнал о том, что тесты можно запускать. Однако есть нюанс - при создании контейнера Docker мгновенно открывает внешний порт для службы, даже если последняя еще не начала прослушивать соответствующий внутренний порт внутри контейнера. Вот почему все тесты будут подключаться сразу, и их попытка прочитать соединение приведет к EOF. Когда сервис открывает внутренний порт, тестовая среда сможет отправить запрос, и только тогда мы сможем считать сервис готовым к работе.
  3. Запросить статус услуги. В ответ на запрос статуса сервис мгновенно открывает свои порты и возвращает «Готово» при загрузке и «Не готов», если нет. В наших тестах мы будем время от времени запрашивать статус сервиса, и как только он вернет «Готово», мы сможем сразу перейти к этапу тестирования.
  4. Зарегистрируйтесь в стороннем сервисе или базе данных. Регистрируем услуги в Consul. Вот что может помочь:
  • Как только услуга появится в Consul, она готова. За состоянием службы можно следить с помощью блокирующего запроса с тайм-аутом. Как только услуга зарегистрирована, Consul возвращает информацию об изменении статуса услуги.
  • Статус услуги можно проанализировать, контролируя ее проверку. Фреймворк для интеграционного тестирования получает информацию о новой услуге от Consul (как только что описано) и начинает отслеживать изменения ее статуса. Как только статусы всех проверок сервиса поменяются на «пройден», сервис можно считать готовым к работе.

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

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

Запуск всех служб

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

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

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

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

При тестировании

При проведении тестов нам действительно хочется проникнуть в нашу инфраструктуру и потратить время на то, чтобы поиграть с ней. Если не сейчас, то когда?

  • Изменение конфигурации службы

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

  • Добавление новой услуги

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

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

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

Пост-тестирование

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

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

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

Отладка

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

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

  • Во-первых, мы добавляем к сервису команду «log_notice» и, получив ее, он записывает в свой журнал сообщение из запроса.
  • Перед запуском теста мы отправляем «log_notice», содержащий имя теста, всем работающим службам. И мы повторяем то же самое после завершения теста.

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

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

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

Если мы настроим конфигурацию фреймворка на сохранение инфраструктуры после выполнения всех тестов в пакете, мы получим полный доступ к системе. Мы сможем проверять состояние сервера, получать с него данные, отправлять различные запросы, анализировать служебные файлы на диске, а также использовать gdb / strace / tcpdump и профилирование. Затем мы сформируем гипотезу, перекомпилируем изображение, запустим тесты и итеративно определим корень проблемы.

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

Ускорение

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

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

  • Мы можем группировать тесты только для чтения и запускать их параллельно в рамках одного теста (это действительно легко сделать в Go, благодаря горутинам). Эти тесты будут работать с изолированным набором данных.
  • Мы можем предложить обширную конфигурацию сервиса - таким образом, в тестах мы сможем установить меньшие значения тайм-аута, что, в свою очередь, сократит время тестирования.
  • Сервисы можно запускать в необходимой и достаточной конфигурации. Например, если в некоторых случаях на этапе производства служба запускается с 4 шардами, тогда как для определенного теста требуется только факт мульти-сегментированности, подойдет всего 2 шарда.
  • Одновременно можно запускать несколько инфраструктур тестирования (при условии наличия ресурсов). По сути, это параллельный запущенный набор тестов.
  • Контейнеры можно использовать повторно.
  • Мы можем спросить себя, действительно ли контейнеру нужна новая услуга или будет достаточно мока. И макеты - это не сопрягающие макеты, которые мы используем в модульном тестировании, а независимые серверы. Макет выдает себя за одну из наших служб и знает, как следовать его протоколу. Другие службы, работающие в текущей тестовой инфраструктуре, не могут отличить ее от исходной службы. Мок позволяет нам установить шаблон поведения для реальной службы, не запуская ее в контейнере.

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

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

handler := func(request *serviceProto.Message) mock.Result {
    statsRequest, ok := request.(*serviceProto.StatsRequest)
    // Checking request
    return mock.Result{
        Msg: PrepareResponse(statsRequest),
        Action: mock.ActionWriteResponse,
    }
}
serviceMock.Start(listenAddr, serviceProto, handler)
defer serviceMock.Stop()

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

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

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

Выполнение

Для тех, кто интересуется деталями реализации, вот они!

Наш фреймворк находится в одном репозитории с тестируемыми сервисами - в независимом каталоге «автотестов», который состоит из нескольких модулей:

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

«Мок» содержит реализацию фиктивного сервера для каждой службы, не являющейся сторонней.

«Люкс» включает в себя общую реализацию. Он умеет работать с сервисами, как ждать загрузки, как проверять работоспособность сервиса и многое другое.

«Среда» хранит данные о текущей тестовой среде (запущенных службах) и отвечает за сеть.

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

Помимо модулей фреймворка, у нас был 21 набор тестов, доступных на тот момент для службы M, когда эта статья была скомпилирована, в частности, набор тестов Smoke. Каждый из них смог создать свою инфраструктуру с необходимым набором сервисов. Тесты хранились в файлах в комплекте тестов. У нас есть около 1980 тестов для службы M, и на сборку двоичных файлов, создание контейнеров и запуск тестов требуется около 1 часа (фаза тестирования длится около 54 минут).

Для запуска конкретного набора тестов требуется что-то вроде:

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

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

QA

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

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

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

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

Непрерывная интеграция

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

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

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

Подводя итоги

Вот результаты всей работы.

  • Жить стало легче. Теперь до стадии производства осталось меньше проблем с интеграцией, что приводит к более стабильному производству.
  • Мы научились запускать разнообразную инфраструктуру и покрывать больше сценариев за меньшее время.
  • Работаем с инфраструктурой во время тестирования. Таким образом, мы получаем больше возможностей для реализации различных тестовых случаев.
  • Больше ошибок вылавливаем на стадии разработки. Сами разработчики пишут позитивные сценарии, сразу находя некоторые ошибки и устраняя их. Круговые обходы ошибок стали короче.
  • Тестировщикам больше не нужно собирать положительные кейсы. QA-инженеры могут сосредоточиться на более сложных сценариях.
  • Больше никаких блокировок на этапе тестирования, когда задачи для разных сервисов разрабатываются параллельно, все сразу перенаправляются QA-инженерам.
  • Мы скомпилировали MVP для фреймворка интеграционного тестирования быстро, за пару недель, потому что задача оказалась не слишком трудоемкой.
  • Мы пользуемся этим фреймворком больше года.

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

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

  • Более продолжительный тестовый прогон. Системы сложны, и запросы выполняются в нескольких сервисах.
  • Нестабильность, поскольку система построена из асинхронных компонентов. С этим можно и нужно бороться, и мы тоже над этим работаем - наша доля нестабильных тестов приближается к нулю.
  • Сложность написания теста. Вы должны понимать, как система работает в целом, каково ее ожидаемое поведение и как она может выйти из строя.
  • Инфраструктура на тестах и ​​на стадии производства подвержена противоречиям. Если не все службы находятся в контейнерах на этапе производства, тестовая среда не может на 100% соответствовать этапу производства. Фактически, некоторые из наших сервисов на проде не находятся в контейнерах, но до сих пор у нас не было проблем с их тестированием в контейнерах.

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

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

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

Удачи!