Автотестирование микросервисов в Docker для непрерывной интеграции

23/07/20
Технические статьи
Автотестирование микросервисов в Docker для непрерывной интеграции

Микросервисы в настоящее время получили широкое распространение: в финансовых приложениях, стриминговых сервисах, логистике, e-commerce или IoT приложениях – компании по всему миру переходят на микросервиную архитектуру.

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

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

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

В данной статье мы хотели бы поделиться своим опытом разворачивания и написания автоматизированных тестов для микросервисного веб-приложения, работающего в Docker в облаке Amazon AWS. Приложение используется нашим заказчиком для планирования и расчёта стоимости работ, которые он выполняет для своих клиентов. До внедрения автотестирования на проекте уже использовались юнит-тесты, однако довольно часто возникали ошибки, которые юнит-тесты не обнаруживали.

Архитектура проекта

Приложение состоит из более чем десяти сервисов, при этом часть из них написана на .NET Core, а часть – на NodeJs. Каждый сервис работает в Docker-контейнере в Amazon Elastic Container Service.  У каждого своя база Postgres, хотя некоторые микросервисы работают еще и с Redis, при этом общих БД нет. Если нескольким сервисам нужны одни и те же данные, то эти данные в момент их изменения передаются каждому из этих сервисов через SNS (Simple Notification Service) и SQS (Amazon Simple Queue Service), и сервисы сохраняют их в свои обособленные базы.

Еще одна особенность системы: большинство сервисов недоступно напрямую из интернета. Доступ осуществляется через API Gateway, который проверяет права доступа. Это тоже отдельный сервис, который так же надо тестировать.

Помимо перечисленных сервисов, приложение использует SignalR как сервис уведомлений, работающий в реальном времени. Он доступен напрямую из интернета и сам работает с OAuth, потому что встраивать поддержку Web-сокетов в Gateway оказалось нецелесообразно по сравнению с интеграцией OAuth и сервиса уведомлений.

Почему традиционный подход к тестированию не сработал

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

  • тестирование консистентности данных в реляционной БД
  • тестирование работы с облачными сервисами
  • неверные предположения при написании мок-объектов

В процессе разработки стало понятно, что юнит-тестов недостаточно, чтобы своевременно находить все проблемы, да и, помимо этого, наша система использует БД Postges, и добиться автоматического запуска Postgres и выполнения миграции при запуске теста нам так и не удалось.

Изучив все предлагаемые workarounds, мы пришли к выводу, что нужно подойти к проблеме с другой стороны:

  • внедрить интеграционные тесты и, таким образом, работать с настоящей базой данных, а не мок-объектом
  • тестировать целые микросервисы в Docker-контейнере, потому что в юнит-тесте могут быть ошибки при создании начального состояния
  • вместо облачных сервисов использовать их локальные версии, чтобы гарантировать воспроизводимость теста и независимость от интернета
Преимущества тестирования в Docker

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

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

Во-вторых, один и тот же скрипт запускают как сами разработчики на своих Windows-десктопах, так и сервер Gitlab CI под Linux. Таким образом, внедрение новых тестов не требует установки дополнительных инструментов ни на компьютере разработчика, ни на сервере, где тесты запускаются при коммите.

В-третьих, тестовое окружение разворачивается и прогоняет тесты на  локальном сервере. Это позволяет быть уверенным, в том, что автоматический тест в любом случае сработает и от нас не потребуется терять время на поиск причины остановки процесса в логах.  Использование LocalStack избавляет от запросов к Amazon и, таким образом, решает проблему слишком частых запросов к нему.

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

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

Настройка тестового окружения

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

  • Настроить тестируемый сервис на локальное окружение, в переменных окружения указываются реквизиты для подключения к базе и AWS.
  • Запустить Postgres и выполнить миграцию, запустив Liquibase.

В реляционных СУБД прежде, чем записывать данные в базу, нужно создать схему данных, проще говоря, таблицы. При обновлении приложения таблицы нужно привести к виду, используемому новой версией, причем, желательно, без потери данных, – это миграция. Создание таблиц в изначально пустой базе – частный случай миграции. Миграцию можно встроить в само приложение. И в .NET, и в NodeJS есть фреймворки для миграции. В нашем случае в целях безопасности микросервисы лишены права менять схему данных, и миграция выполняется с помощью Liquibase.

  • Запустить Amazon LocalStack. Это реализация сервисов AWS для запуска у себя. Для LocalStack есть готовый образ в Docker Hub.
  • Запустить скрипт для создания в LocalStack необходимых сущностей. Shell-скрипты используют AWS CLI.
Как устроен автоматический тест

Во время теста в Docker работает все: и тестируемый сервис, и Postgres, и инструмент для миграции, и Postman, а, вернее, его консольная версия – Newman. Docker решает целый ряд проблем:

  • Независимость от конфигурации хоста
  • Установка зависимостей: докер скачивает образы с Docker Hub
  • Возврат системы в исходное состояние: просто удаляем контейнеры

Docker-compose объединяет контейнеры в виртуальную сеть, изолированную от интернета, в которой контейнеры находят друг друга по доменным именам.

Тестом управляет shell-скрипт. Для запуска теста под Windows используем git-bash. Таким образом, достаточно одного скрипта и для Windows, и для Linux. Git и Docker установлены у всех разработчиков на проекте. При установке Git под Windows устанавливается git-bash, так что он тоже у всех есть.

Скрипт выполняет следующие шаги:

  • Построение докер-образов
    • docker-compose build
  • Запуск БД и LocalStack
    • docker-compose up -d <контейнер>
  • Миграция БД и подготовка LocalStack
    • docker-compose run <контейнер>
  • Запуск тестируемого сервиса
    • docker-compose up -d <сервис>
  • Запуск теста (Newman)
  • Остановка всех контейнеров
    • docker-compose down
  • Постинг результатов в Slack
    • У нас есть чат, куда попадают сообщения с зеленой галочкой или красным крестиком и ссылкой на лог.

В этих шагах задействованы следующие Docker-образы:

  • Тестируемый сервис – тот же образ, что и для продакшена. Конфигурация для теста – через переменные окружения.
  • Для Postgres, Redis и LocalStack используются готовые образы из Docker Hub. Для Liquibase и Newman тоже есть готовые образы. Мы строим свои на их остове, добавляя туда наши файлы.
  • Для подготовки LocalStack используется готовый образ AWS CLI, и на его основе создается образ, содержащий скрипт.

Используя volumes, можно не строить Docker-образ только для добавления файлов в контейнер. Однако, volumes не годятся для нашего окружения, потому что задачи Gitlab CI сами работают в контейнерах. Из такого контейнера можно управлять докером, но volumes там не работают.

Сложности, которые мы преодолели

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

  • конфликты параллельных задач в одном Docker-хосте
  • конфликты идентификаторов в БД при итерациях теста
  • ожидание готовности микросервисов
  • объединение и вывод логов во внешние системы
  • тестирование исходящих HTTP-запросов
  • тестирование веб-сокетов (с помощью SignalR)
  • тестирование аутентификации и авторизации OAuth

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

В заключение

Решение развернуть автоматизированное тестирование с использованием технологий Docker, привело к тому, что у нас появился набор стабильных тестов, в которых каждый сервис работает как единое целое, взаимодействуя с базой данных и с Amazon LocalStack. Эти тесты существенно снижают риск ошибок, неизбежных для проекта по разработке и поддержке приложения со сложным взаимодействием 10+ микросервисов с частыми регулярными деплоями, где работают более 30 разработчиков в разных локациях и удалённо, и позволяют заказчику быстрее получить надежное и качественное решение. В 2016 году компания Puppet провела опрос более 4600 разработчиков и выяснила, что команды, использующие контейнеры совместно с применением DevOps или Agile практик разворачивали новые приложения, в среднем, в 200 раз быстрее, чем в случае, когда использовалась водопадная модель. В нашем проекте, заказчик получил первый релиз системы после 12 месяцев разработки, а количество ошибок на стороне бэкенда не превышало 1 на 20 коммитов.

Свяжитесь с нами напрямую

Офисы

Москва

117587, Варшавское ш., д. 125, стр. 16А

Ростов-на-Дону

344002, пр. Буденновский, д. 9, офис 305

Нижний Новгород

603104, ул. Нартова, д. 6, корп. 6, офис 829