Привет, Хабр! Меня зовут Ира, я 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 на стероидах не справится сходу – рассказывайте о таких в комментах!

Комментарии (2)


  1. discoverer-official
    00.00.0000 00:00
    +1

    Спасибо за интересную статью.

    Подскажите пожалуйста чем и как вы нагружали систему когда почувствовали проблему с производительностью?


    1. milka_root Автор
      00.00.0000 00:00

      нагрузку на систему подавали тестами с использованием jmeter-java-dsl
      проблемы, о которых говорю в статье - это отказ заглушки по OOM
      здесь намеренно не даю конкретных графиков и схем подачи нагрузки, дабы не отвлекать читателя от основного контекста