Какие тесты выбрать для облака? Сравниваем варианты

Привет, Хабр! Меня зовут Илья Казначеев, я техлид в MTS Cloud, это облачный провайдер МТС. Моя команда занимается сервисом Kubernetes Managed, а еще мы проводим тесты облачных платформ. В этой статье я расскажу о нашем опыте: какие виды тестов мы пробовали, как боролись с проблемами и к чему в итоге пришли.

gkaiuwl5xsbsxjorukjo7f-gmus.jpeg

Что такое облако


Облачная платформа — это набор виртуальных машин и инфраструктуры для них. В комплекте: сети, подсети, правила маршрутизации, фаерволы и виртуальные диски. Это основа облачного провайдера, Infrastructure as a Service (IaaS). На базовом уровне облака можно покупать или арендовать эти или другие ресурсы: базы данных, Kubernetes, Hadoop.

dkmr0oan4q9d0aqurf2t86ancia.jpeg

Строить сервисы над базовым уровнем сложнее. Тут нужна уверенность в работоспособности всех сервисов, а не только вашего. Пример — PaaS, который работает на базе инфраструктуры Kubernetes, а именно — группа master node, на них функционируют management plane: kube-apiserver, scheduler, controller-manager и база данных. Они управляют кластером Kubernetes и распределяют нагрузки. Есть также группа worker node, на которых эти нагрузки исполняются.

При запуске контейнера Kubernetes последний «бежит» на одной из этих nodes. Начинается общение: мастера объединяются в кворум, обмениваются данными. Также мастер общается с воркерами и говорит им, что делать, и как быть, если какая-то из нод сломалась. Все происходит внутри сети, и такой кластер имеет load balancer, который раздает трафик на мастер ноды.

Kubernetes мы не можем просто разворачивать поверх инфраструктуры через Terraform, этот процесс сложно контролировать. Что-то может пойти не так и мы не сможем оперативно отреагировать. А реагировать нужно, поэтому у нас есть последовательный процесс, который выглядит так:

image-loader.svg

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

  • Инициализируем, создаем и настраиваем сеть
  • Создаем и настраиваем master node
  • Конфигурируем management plane
  • Создаем и запускаем worker node
  • Доконфигурируем и дозапускаем оставшиеся ресурсы
  • Проверяем готовность и отдаем кластер клиенту


Как мы тестировали?


Есть такая вещь, как пирамида тестирования, она показывает соотношение количества тестов в проекте. Внизу Unit-тесты, которых должно быть большинство, дальше идут интеграционные тесты, потом End-to-end, функциональные и другие тесты. Их дороже писать и выполнять, но таких проверок нужно меньше, потому что почти все покрывается Unit-тестами. Именно с такой схемы мы и начали.

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

Какие недостатки мы нашли?


Разберем каждый тип тестов отдельно:

Unit-тесты: в целом хороши, но есть нюансы. Первое — они не позволяют покрыть многошаговый процесс. Второе — из-за специфики наших процессов Unit-тесты часто неэффективны: у нас нет каких-то отдельных маленьких функций, зато присутствует доменная логика. Мы разрабатываем сервисы в рамках domain driven design, и логика объединена вокруг доменов и агрегаторов. Для каждого шага нужно подготовить состояние домена, потом выполнить шаг и провести проверку. Unit тесты плохо ложатся на DDD, потому что в нашем случае в DDD очень много приходится работать с состояниями. Можно ли покрыть это интеграционными тестами?

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

End-to-end тесты. Они хороши, но их тоже долго и сложно писать. Такие тесты тяжело поддерживать: при каждом изменении сервисов нужно менять и тесты. Частота изменения примерно равна количеству тестов, возведенному в степень количества сервисов. Кроме того, почти невозможно поднимать все окружение за раз. Если ваш сервис работает с инфраструктурой виртуализации — вы не можете просто эмулировать эту инфраструктуру, придется запускать виртуальные машины и сети, что дорого и не всегда возможно.

А как же API-тесты, спросите вы? Тут все неоднозначно: наше API довольно тонкое, есть несколько rest-сущностей, с которыми работает фронтенд, а за ними скрывается пласт технической, инфраструктурной и бизнес-логик, которые меняются быстрее, чем API. Кроме того, не все работает через API — есть часть процессов внутри сервиса: autoscaling, autohealing, работа с persistent volume.

Итак, мы выяснили, что:

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

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

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

Решение проблемы


Выход нашелся — это тесты на основе behavior-driven development (BDD). Для них мы описываем бизнес-процессы так, как их видят клиент и product owner, а потом для этого пишем код. Синтаксис Gherkin, который вырос из фреймворка Cucumber, как раз описывает сценарий, шаги и ожидания тем языком, которым разговаривает владелец домена.

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

Пример — кластер, который пользователь создает в конфигурации high availability и три группы nodes с публичным доступом. На каждом уровне можно добавить ветвление, что групп nodes будет не три, а, например, пять. Также такое описание шагов помогает в анализе теста — когда он падает, мы видим цепочку шагов с помощью описания процесса. И чтобы это пофиксить, даже не обязательно открывать тест — можно сразу по коду найти, в чем проблема.

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

18iokixze_z0l72xwuvlpkgizf4.jpeg

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

Как мы попробовали godog


Наши сервисы написаны на Go, поэтому мы искали решение именно среди go-библиотек. Первым попробовали godog, это библиотека фреймворка Cucumber.

k5ediekhsyaqooljmobsftly-ik.jpeg

Она нам не понравилась. И вот почему:

  • Godog генерирует тесты по gherkin-коду, гибкость получается невысокой — нет глубокой вложенности, сложно описывать процессы.
  • Все нужно вносить в Gherkin, а это специфический синтаксис. В результате в проекте появился дополнительный dsl, который нам не был нужен.
  • Сгенерированный код плохо читаем, с ним неудобно работать. Когда у вас десятки тысяч строк тестового кода — чтение превращается в сложное занятие.
  • При генерации нарушается последовательность шагов. Функции для тестов генерятся не в той последовательности, как они описаны, а в алфавитном порядке. При этом они путаются, функциями невозможно пользоваться.
  • Неудобно работать с состоянием между шагами.


Все это это привело к тому, что мы от godoc отказались и решили протестировать библиотеку Ginkgo. Она создана как раз для BDD тестирования.

j_xprvdyifs0jhjnej7g1zklqsa.jpeg

Резюме такое:

  • Есть изначальная генерация основы тестов, что удобно.
  • Тесты оформляются через функции с ключевыми словами, выглядит как дерево сценариев.
  • Удобно работать с состоянием между шагов, причем даже если сценарий древовидный.
  • Библиотека позволяет легко настроить контекст теста. Можно задавать моки перед каждым шагом, на каждом узле в этом дереве сценариев.
  • Дает понятный и красивый вывод при ошибках.
  • В библиотеке есть множество вариантов настройки: как будут проходить тесты, обрабатываться и показываться ошибки, какой будет вывод.
  • Хорошая реализация setup и teardown, чего в стандартной библиотеке нет.


Если кому-то этого мало — существует библиотека gomega для продвинутой валидации результатов.

Вот как выглядит короткий тест:

wwoj_w9l-93khphttarkjjyb5e0.jpeg

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

Недостатки BDD


Выходит, что BDD-тесты — это идеальный вариант? К сожалению, нет. Вот какие недостатки мы выявили:

Из-за древовидной структуры тесты очень сильно уезжают вправо.

Такие тесты очень длинные (у нас больше 10 000 строк тестов), в них тяжело ориентироваться.

Изменения в коде требуют длительных изменений в тестах: изменение в 20 минут, а тесты приходится исправлять два часа.

Новым разработчикам нелегко разобраться в этих тестах. Сам тест при этом выглядит так: описание контекста, описание какого-то шага, тест этого шага, задание моков, задание preconditions, выполнение и проверка кода, опять описание шага, задание preconditions и выполнение кода. При создании кластера таких шагов 20 и они описаны в огромной «простыне». Если такого 10 000 строк — представляете, как это выглядит?

Как мы справились с проблемами?


Вынесли каждый шаг в отдельную функцию.

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

Из «простыни» (слева) это превратилось в такой упорядоченный код (справа):

kj7a6qm1b1tkqliwchebpmbz7y8.jpeg

Некоторые проблемы, конечно, остались. BDD-тесты все равно дорогие и они сложнее, чем Unit-тесты, поэтому от последних мы окончательно не отказались. Для серьезных задач BDD-тесты все равно требуют «доработки напильником». Мы поверх ginkgo-библиотеки накрутили своих хелперов, логики и небольших улучшений. Они позволили реализовать красивую структуру, где сначала идут блоки с шагами и с тестами для каждого маленького шага, а дальше — конструктор. В нем из шагов составляются процессы, которые мы и тестируем.

Что получилось


Мы написали более 10 000 строк BDD-тестов только в одном микросервисе. Нам важно покрыть «горячие» участки, и это покрытие у нас большое. Ginkgo мы также используем для интеграционных тестов. Разделение тестового кода и описания шагов упростило понимание тестов разработчиками. Теперь они могут сразу открывать часть, где написаны сценарии и шаги, без описания самих тестов, а потом провалиться в нужный тест, в котором уже будут описаны проверки, моки. Писать тесты все еще сложно, долго и дорого, но улучшения есть. Ну и, наконец, BDD тесты позволили выявить очень много серьезных ошибок на ранних стадиях, когда это еще не уехало в продуктив. Поэтому такая практика полностью оправдала себя.

А с какими проблемами вы сталкивались при покрытии облачных платформ тестами? Какие инструменты для них вы предпочитаете? Расскажите о своем опыте в комментариях!

Полная версия рассказа Ильи Казначеева — в видеоролике ниже.

© Habrahabr.ru