Привет, Хабр! Меня зовут Ира, я SRE в команде Samokat.tech. В этом посте хочу поделиться подходом в тестировании, которым мы пользуемся сами. Если вам при подготовке тестов часто приходится писать похожие друг на друга как две капли воды заглушки (или, как их ещё называют, «моки»), а затем заботиться о том, чтобы их развернуть, то вам может понравиться наше решение. Освободившись от части рутинных забот, вы можете уделить больше времени чему-то более важному.
Разношёрстные и хаотично развёрнутые в окружении заглушки – дорогие и неэффективные. В каждой отдельной заглушке разбирается в лучшем случае только одна команда, чаще только один человек. Со временем при накоплении критической массы они становятся фактически неподдерживаемыми.
Такие заглушки редко переиспользуются и что важнее, никто ими не делится, так как они либо тривиальны, либо написаны не самым лучшим способом (могут оказаться слишком сложными или неоптимальными).
Сложно представить тестировщика в роли доброго самаритянина, раздающего коллегам свои заглушки. Его к этому вряд ли что-то мотивирует, да и работы у него всегда прилично. Некогда отвлекаться! Так, давайте разгрузим немного нашего трудягу.
Общий подход к заглушкам
Системы, как правило, не живут сами по себе, зачастую они имеют множество интеграций, не все из которых будут включены в ваш объект тестирования. Иногда такая связь может указывать на внешний ресурс, который недоступен в вашем окружении. Тогда вооружившись любимым языком программирования, тестировщик приступает к написанию простой заглушки, для которой понадобится всего лишь пример запроса и ответа. В случае когда нам понадобится особая логика от заглушки, такая как задержки отклика или что-то поэкзотичнее (например, динамическое конфигурирование), то её придётся дорабатывать и пересобирать.
Для начала подумаем что хотим от нашего будущего решения:
Прежде всего хочется тратить минимум усилий на написание заглушки. Желательно, чтобы, имея документацию, её мог написать даже человек без особых скилов в разработке.
Важна и возможность переиспользования. Использовать чужие заглушки или их части существенно облегчит этап написания своих.
Хотелось бы иметь единый подход для всех таких заглушек, что позволит не только шарить их, но и компетенцию по ним.
Заодно нужно остаться в привычном стеке. Нам изначально была ближе Java, так как заметная часть бэкенда в Samokat.tech пишется на Kotlin. Деплой сервисов у нас осуществляется в Kubernetes с использованием helm-чартов, тому же принципу будем следовать и с заглушками.
Для начала неплохо, можно приступить к поиску решения.
Шаг 1: Выберем готовое решение (не потому что мы ленивые, хотя ладно, потому)
Зачем писать то, что уже написано за тебя? Вокруг много open-source решений, которые просто настраиваются. У них есть поддержка разных платформ и режимов.
Наши первоначальные минимальные требования к готовому решению были следующими:
имеется широкий функционал и/или потенциал к расширению;
обладает достаточной производительностью, чтобы покрыть текущие потребности;
не сложно в использовании.
На первых порах выбирали между hoverfly, wiremock и mockserver. Hoverfly хоть и казался внешне простым, но не подошёл по функциональности. Wiremock уже был ближе к истине за счёт поддержки Java, но бенчмарк показал, что он не всегда хорошо работает с большими телами ответа. При построении значительного по размеру параметризированного тела для ответа заглушка сразу после запуска выдавала на первый запрос чрезмерно большое время отклика - до нескольких минут.
В итоге пришли к mockserver. У него оказалась довольно неплохая документация, также он поддерживает несколько вариантов конфигурации: вызов API, с помощью конфиг-файла или же можно описывать формат работы на Java или JavaScript. Поведение mockserver определяется набором конфигураций, в которых описываются ожидаемые на вход запросы и формат ответа на них. Каждая такая конфигурация называется ожиданием или expectation. Они имеют широкий спектр параметров, а также поддерживают регулярные выражения.
Пример ожидания в mockserver:
new MockServerClient("localhost", 1080)
.when(
request()
// соответствует любым запросам, path которых начинается c "/some"
.withPath("/some.*")
)
.respond(
response()
.withBody("some_response_body")
);
На самом деле мы используем только часть функциональности среди множества режимов и возможностей. По началу производительность была неплоха, но далека от идеала. Однако, после отключения подробного логирования поднялась на приемлемый уровень.
Шаг 2: Делаем обёртку для удобства шаринга
Когда мы нашли удобное решение, многие команды уже имели какие-то заглушки. Часть из них была в виде отдельных сервисов, другие же были написаны с использованием фреймворков, но они оставались несистемными и сложными в поддержке.
Нам нужно было предоставить удобный способ работы с mockserver, чтобы расширить его использование. Большим плюсом стал тот факт, что подавляющее число наших тестировщиков могут хорошо работать с кодом.
Тогда мы подготовили решение-обёртку, использующее библиотеку mockserver. Поскольку нам было важно переиспользовать заглушки, мы разместили всё в одном репозитории.
В отдельном классе для каждой команды определяется набор ожиданий (конфигураций). При запуске заглушка инициализирует только ожидания, определённые в классе запускающей команды. Часто мы применяем ожидания, использующие дополнительный callback класс для более гибкого формирования ответа.
Пример ожидания с использованием callback класса:
public class CallbackActionExamples {
public void respondClassCallback() {
new ClientAndServer(1080)
.when(
request()
// соответствует любым запросам, path которых начинается c "/some"
.withPath("/some.*")
)
// заглушка будет отдавать результат выполнения TestExpectationResponseCallback.class
.respond(
HttpClassCallback.callback()
.withCallbackClass(TestExpectationRespondCallback.class)
);
}
public static class TestExpectationResponseCallback implements ExpectationResponseCallback {
// переопределим формирование ответа
@Override
public HttpResponse handle(HttpRequest httpRequest) {
String randomNumber = String.valueOf(new Random().nextInt(1000000));
// вернём простой параметризированный ответ
return response().withBody(String.format("Random body # %s", randomNumber));
}
}
}
Прелесть в том, что вы можете повторно использовать методы, общие для разных команд. Например, единожды описав метод ожидания для авторизации, его можно использовать в классах разных команд.
Осталось описать общий чарт для развёртывания в кубе и мы получаем способ быстро поднимать описанные заглушки на любом стенде.
Шаг 3: Добавляем нужные фичи
Ещё несколько реверансов в сторону встроенной функциональности mockserver. Для некоторых задач оказалась особенно полезной возможность добавить ожидание на лету. Неважно какая конфигурация задана у заглушки, с помощью обращения к API возможно изменить её поведение без рестарта. Чаще всего такие ожидания имеют довольно простую логику, однако, и тут можно сделать что-то невероятное с помощью JavaScript.
Пример ожидания через API:
curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
"httpRequest" : {
"method" : "P.*{2,3}"
},
"httpResponse" : {
"body" : "some_response_body"
}
}'
Для отладки каждая заглушка имеет встроенный дашборд по пути /mockserver/dashboard.
Мы также добавили в нашу обёртку сбор основных метрик заглушки с помощью micrometer. У решения высокий потенциал к расширению, например, мы также добавили поддержку почтового протокола smtp и работаем над полноценной поддержкой graphql.
Для запросов graphql может оказаться недостаточной базовая функциональность определения ожиданий, ведь все запросы имеют одинаковый uri и метод (например, POST /graphql). Усложняет ситуацию то, что тело запроса будет динамически меняться в зависимости от схемы и глубины запроса. Однако в первом приближении удалось побороть и такие сложности, написав универсальный класс на основе библиотеки graphql, который парсит тело запроса и динамически формирует ответ.
Попробуйте это сами
Мы реализовали всё, к чему изначально стремились: заглушки написать просто, они сами и подходы к их разработке активно переиспользуются ребятами из соседних команд.
Итоговое решение оказалось довольно лёгким в поддержке. Имея единый репозиторий, удобно отслеживать изменения по любым заглушкам. Полное отсутствие конфликтов в коде достигается верхнеуровневым ревью.
По ссылке можете найти репозиторий с исходниками решения для реализации нашего похода.
Мы продолжаем совершенствовать наш заглушечный контур. Сейчас мы имеем удобный инструмент, помогающий ускорить тестирование каждый день. Возможно у вас есть примеры задач, с которыми наш mockserver на стероидах не справится сходу – рассказывайте о таких в комментах!
discoverer-official
Спасибо за интересную статью.
Подскажите пожалуйста чем и как вы нагружали систему когда почувствовали проблему с производительностью?
milka_root Автор
нагрузку на систему подавали тестами с использованием jmeter-java-dsl
проблемы, о которых говорю в статье - это отказ заглушки по OOM
здесь намеренно не даю конкретных графиков и схем подачи нагрузки, дабы не отвлекать читателя от основного контекста