Привет, Хабр! Меня зовут Илья Казначеев, я техлид в MTS Cloud, это облачный провайдер МТС. Моя команда занимается сервисом Kubernetes Managed, а еще мы проводим тесты облачных платформ. В этой статье я расскажу о нашем опыте: какие виды тестов мы пробовали, как боролись с проблемами и к чему в итоге пришли.
Облачная платформа – это набор виртуальных машин и инфраструктуры для них. В комплекте: сети, подсети, правила маршрутизации, фаерволы и виртуальные диски. Это основа облачного провайдера, Infrastructure as a Service (IaaS). На базовом уровне облака можно покупать или арендовать эти или другие ресурсы: базы данных, Kubernetes, Hadoop.
Строить сервисы над базовым уровнем сложнее. Тут нужна уверенность в работоспособности всех сервисов, а не только вашего. Пример – PaaS, который работает на базе инфраструктуры Kubernetes, а именно – группа master node, на них функционируют management plane: kube-apiserver, scheduler, controller-manager и база данных. Они управляют кластером Kubernetes и распределяют нагрузки. Есть также группа worker node, на которых эти нагрузки исполняются.
При запуске контейнера Kubernetes последний «бежит» на одной из этих nodes. Начинается общение: мастера объединяются в кворум, обмениваются данными. Также мастер общается с воркерами и говорит им, что делать, и как быть, если какая-то из нод сломалась. Все происходит внутри сети, и такой кластер имеет load balancer, который раздает трафик на мастер ноды.
Kubernetes мы не можем просто разворачивать поверх инфраструктуры через Terraform, этот процесс сложно контролировать. Что-то может пойти не так и мы не сможем оперативно отреагировать. А реагировать нужно, поэтому у нас есть последовательный процесс, который выглядит так:
Это схема создание кластера Kubernetes в облачной инфраструктуре. Некоторые шаги и ветвления опущены, но общая логика такая:
Есть такая вещь, как пирамида тестирования, она показывает соотношение количества тестов в проекте. Внизу 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 позволяют задать контекст теста. На каждом шаге мы можем заполнить моки соответствующими данными, что очень удобно.
Тест описывает реальный процесс в терминах домена с точки зрения того, как пользователь будет видеть этот процесс.
Наши сервисы написаны на Go, поэтому мы искали решение именно среди go-библиотек. Первым попробовали godog, это библиотека фреймворка Cucumber.
Она нам не понравилась. И вот почему:
Все это это привело к тому, что мы от godoc отказались и решили протестировать библиотеку Ginkgo. Она создана как раз для BDD тестирования.
Резюме такое:
Если кому-то этого мало – существует библиотека gomega для продвинутой валидации результатов.
Вот как выглядит короткий тест:
Выводы об ошибке наглядны: полностью тот же текст, последовательности, но конкретно видно, что сломалось. Это очень удобно и можно интегрировать с G-Unit.
Выходит, что BDD-тесты – это идеальный вариант? К сожалению, нет. Вот какие недостатки мы выявили:
Из-за древовидной структуры тесты очень сильно уезжают вправо.
Такие тесты очень длинные (у нас больше 10 000 строк тестов), в них тяжело ориентироваться.
Изменения в коде требуют длительных изменений в тестах: изменение в 20 минут, а тесты приходится исправлять два часа.
Новым разработчикам нелегко разобраться в этих тестах. Сам тест при этом выглядит так: описание контекста, описание какого-то шага, тест этого шага, задание моков, задание preconditions, выполнение и проверка кода, опять описание шага, задание preconditions и выполнение кода. При создании кластера таких шагов 20 и они описаны в огромной «простыне». Если такого 10 000 строк – представляете, как это выглядит?
Вынесли каждый шаг в отдельную функцию.
Реализация шагов – в первую очередь. Сначала мы описываем, что мы делаем, затем описываем все сценарии, используя эти шаги. И за счет функционала ginkgo на определенном шаге можно выполнить набор заранее заданных проверок — это нам позволило на каждом шаге помимо теста выполнять еще какие-то глобальные проверки. Например, квотирование. Тесты из витиеватой поэмы превратились в конструктор, где мы для каждого случая могли блоками задавать, что у нас происходит.
Из «простыни» (слева) это превратилось в такой упорядоченный код (справа):
Некоторые проблемы, конечно, остались. BDD-тесты все равно дорогие и они сложнее, чем Unit-тесты, поэтому от последних мы окончательно не отказались. Для серьезных задач BDD-тесты все равно требуют «доработки напильником». Мы поверх ginkgo-библиотеки накрутили своих хелперов, логики и небольших улучшений. Они позволили реализовать красивую структуру, где сначала идут блоки с шагами и с тестами для каждого маленького шага, а дальше – конструктор. В нем из шагов составляются процессы, которые мы и тестируем.
Мы написали более 10 000 строк BDD-тестов только в одном микросервисе. Нам важно покрыть «горячие» участки, и это покрытие у нас большое. Ginkgo мы также используем для интеграционных тестов. Разделение тестового кода и описания шагов упростило понимание тестов разработчиками. Теперь они могут сразу открывать часть, где написаны сценарии и шаги, без описания самих тестов, а потом провалиться в нужный тест, в котором уже будут описаны проверки, моки. Писать тесты все еще сложно, долго и дорого, но улучшения есть. Ну и, наконец, BDD тесты позволили выявить очень много серьезных ошибок на ранних стадиях, когда это еще не уехало в продуктив. Поэтому такая практика полностью оправдала себя.
А с какими проблемами вы сталкивались при покрытии облачных платформ тестами? Какие инструменты для них вы предпочитаете? Расскажите о своем опыте в комментариях!
Полная версия рассказа Ильи Казначеева – в видеоролике ниже.
Что такое облако
Облачная платформа – это набор виртуальных машин и инфраструктуры для них. В комплекте: сети, подсети, правила маршрутизации, фаерволы и виртуальные диски. Это основа облачного провайдера, Infrastructure as a Service (IaaS). На базовом уровне облака можно покупать или арендовать эти или другие ресурсы: базы данных, Kubernetes, Hadoop.
Строить сервисы над базовым уровнем сложнее. Тут нужна уверенность в работоспособности всех сервисов, а не только вашего. Пример – PaaS, который работает на базе инфраструктуры Kubernetes, а именно – группа master node, на них функционируют management plane: kube-apiserver, scheduler, controller-manager и база данных. Они управляют кластером Kubernetes и распределяют нагрузки. Есть также группа worker node, на которых эти нагрузки исполняются.
При запуске контейнера Kubernetes последний «бежит» на одной из этих nodes. Начинается общение: мастера объединяются в кворум, обмениваются данными. Также мастер общается с воркерами и говорит им, что делать, и как быть, если какая-то из нод сломалась. Все происходит внутри сети, и такой кластер имеет load balancer, который раздает трафик на мастер ноды.
Kubernetes мы не можем просто разворачивать поверх инфраструктуры через Terraform, этот процесс сложно контролировать. Что-то может пойти не так и мы не сможем оперативно отреагировать. А реагировать нужно, поэтому у нас есть последовательный процесс, который выглядит так:
Это схема создание кластера 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 позволяют задать контекст теста. На каждом шаге мы можем заполнить моки соответствующими данными, что очень удобно.
Тест описывает реальный процесс в терминах домена с точки зрения того, как пользователь будет видеть этот процесс.
Как мы попробовали godog
Наши сервисы написаны на Go, поэтому мы искали решение именно среди go-библиотек. Первым попробовали godog, это библиотека фреймворка Cucumber.
Она нам не понравилась. И вот почему:
- Godog генерирует тесты по gherkin-коду, гибкость получается невысокой — нет глубокой вложенности, сложно описывать процессы.
- Все нужно вносить в Gherkin, а это специфический синтаксис. В результате в проекте появился дополнительный dsl, который нам не был нужен.
- Сгенерированный код плохо читаем, с ним неудобно работать. Когда у вас десятки тысяч строк тестового кода — чтение превращается в сложное занятие.
- При генерации нарушается последовательность шагов. Функции для тестов генерятся не в той последовательности, как они описаны, а в алфавитном порядке. При этом они путаются, функциями невозможно пользоваться.
- Неудобно работать с состоянием между шагами.
Все это это привело к тому, что мы от godoc отказались и решили протестировать библиотеку Ginkgo. Она создана как раз для BDD тестирования.
Резюме такое:
- Есть изначальная генерация основы тестов, что удобно.
- Тесты оформляются через функции с ключевыми словами, выглядит как дерево сценариев.
- Удобно работать с состоянием между шагов, причем даже если сценарий древовидный.
- Библиотека позволяет легко настроить контекст теста. Можно задавать моки перед каждым шагом, на каждом узле в этом дереве сценариев.
- Дает понятный и красивый вывод при ошибках.
- В библиотеке есть множество вариантов настройки: как будут проходить тесты, обрабатываться и показываться ошибки, какой будет вывод.
- Хорошая реализация setup и teardown, чего в стандартной библиотеке нет.
Если кому-то этого мало – существует библиотека gomega для продвинутой валидации результатов.
Вот как выглядит короткий тест:
Выводы об ошибке наглядны: полностью тот же текст, последовательности, но конкретно видно, что сломалось. Это очень удобно и можно интегрировать с G-Unit.
Недостатки BDD
Выходит, что BDD-тесты – это идеальный вариант? К сожалению, нет. Вот какие недостатки мы выявили:
Из-за древовидной структуры тесты очень сильно уезжают вправо.
Такие тесты очень длинные (у нас больше 10 000 строк тестов), в них тяжело ориентироваться.
Изменения в коде требуют длительных изменений в тестах: изменение в 20 минут, а тесты приходится исправлять два часа.
Новым разработчикам нелегко разобраться в этих тестах. Сам тест при этом выглядит так: описание контекста, описание какого-то шага, тест этого шага, задание моков, задание preconditions, выполнение и проверка кода, опять описание шага, задание preconditions и выполнение кода. При создании кластера таких шагов 20 и они описаны в огромной «простыне». Если такого 10 000 строк – представляете, как это выглядит?
Как мы справились с проблемами?
Вынесли каждый шаг в отдельную функцию.
Реализация шагов – в первую очередь. Сначала мы описываем, что мы делаем, затем описываем все сценарии, используя эти шаги. И за счет функционала ginkgo на определенном шаге можно выполнить набор заранее заданных проверок — это нам позволило на каждом шаге помимо теста выполнять еще какие-то глобальные проверки. Например, квотирование. Тесты из витиеватой поэмы превратились в конструктор, где мы для каждого случая могли блоками задавать, что у нас происходит.
Из «простыни» (слева) это превратилось в такой упорядоченный код (справа):
Некоторые проблемы, конечно, остались. BDD-тесты все равно дорогие и они сложнее, чем Unit-тесты, поэтому от последних мы окончательно не отказались. Для серьезных задач BDD-тесты все равно требуют «доработки напильником». Мы поверх ginkgo-библиотеки накрутили своих хелперов, логики и небольших улучшений. Они позволили реализовать красивую структуру, где сначала идут блоки с шагами и с тестами для каждого маленького шага, а дальше – конструктор. В нем из шагов составляются процессы, которые мы и тестируем.
Что получилось
Мы написали более 10 000 строк BDD-тестов только в одном микросервисе. Нам важно покрыть «горячие» участки, и это покрытие у нас большое. Ginkgo мы также используем для интеграционных тестов. Разделение тестового кода и описания шагов упростило понимание тестов разработчиками. Теперь они могут сразу открывать часть, где написаны сценарии и шаги, без описания самих тестов, а потом провалиться в нужный тест, в котором уже будут описаны проверки, моки. Писать тесты все еще сложно, долго и дорого, но улучшения есть. Ну и, наконец, BDD тесты позволили выявить очень много серьезных ошибок на ранних стадиях, когда это еще не уехало в продуктив. Поэтому такая практика полностью оправдала себя.
А с какими проблемами вы сталкивались при покрытии облачных платформ тестами? Какие инструменты для них вы предпочитаете? Расскажите о своем опыте в комментариях!
Полная версия рассказа Ильи Казначеева – в видеоролике ниже.