Привет! Меня зовут Ольга Инеева, я ведущий инженер по обеспечению качества в Т-Банке. Расскажу о проблемах тестирования интеграции и об инструменте для мокирования Mockingbird. Мы решили проблему сложных связанных сценариев и хотим поделиться этим знанием. Добро пожаловать под кат!

Проблемы тестирования интеграций

Наше подразделение занимается продуктами, связанными с BNPL-политикой (buy now, pay later). Это такие продукты, как автокредит, ипотека, сервис «Долями», потребительские кредиты и другое. У нас много интеграций — как внутрибанковских, так и внешних. На стабильность работы внешних систем мы чаще всего не можем повлиять.

Мы столкнулись со множеством проблем при тестировании этих интеграций:

  • Сторонняя система еще не реализована, а мы уже хотим протестировать взаимодействие с ней.

  • Тестовый контур приложения, с которым мы интегрируемся, работает нестабильно.

  • Долгий ответ внешнего сервиса. Можно отвлечься на другие задачи, пока ждешь ответа, а писать автотесты через Thread.sleep(N) — не лучшая идея. 

  • Сложность в получении ответа, для которого нужны специфические данные.

  • Необходимость ручных действий соседней команды. Неудобно постоянно просить коллегу нажать кнопку или прописать данные.

  • Нехватка тестовых контуров. Бывают случаи, когда тестовый контур у системы отсутствует, или каждый поход в нее платный, что сильно сокращает возможности тестирования.

В подобных случаях лучше использовать моки. Моки, или заглушки, –— это эмуляция сервиса, с которым у нас есть интеграция. Главное правило: не увлечься ими, протестировать в рамках e2e-сценариев и живой интеграции, иначе рискуем словить непредвиденный баг. 

Плюсы мокирования:

  • Тесты становятся быстрее. Интеграции на тестовых контурах отвечают с задержкой, а развернутый мок-сервер — практически мгновенно.

  • Возможно сэмулировать все тестовые случаи.

  • Растет уровень доверия к тестам.

Минусы:

  • Риск словить баг на реальной интеграции.

  • Необходимо поддерживать моки в актуальном состоянии. Можно попасть в ситуацию, когда другой сервис обновил контракт в одностороннем порядке, а тесты остались «зелеными». От этого нас могут защитить контрактные тесты, но это уже совсем другая история.

Уровень доверия к тестам

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

113 зеленых тестов — успешные, 3 красных — упавшие
113 зеленых тестов — успешные, 3 красных — упавшие

Вы думаете о том, почему упали 3 теста. Причин может быть много: у стороннего сервиса мог быть сбой, возможно, есть баг со стороны интеграции или тест действительно нашел проблему нашего приложения. 

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

Я собрала самые используемые решения для мокирования в таблицу.

Mountebank

WireMock

REST

REST

SOAP

SOAP

gRPC (плагин)

Webhooks

WebSocket

Мокирование цепочки вызовов

Нам не хватило функций в этих популярных решениях:

  • Эмуляции работы с брокерами сообщений.

  • Поддержки сложных связанных сценариев, в том числе между HTTP-моками и моками для брокеров сообщений.

  • Приоритизации моков: хочется иметь и постоянный контур для Happy Path, и возможность тестировать различные сценарии с помощью моков, не ломая при этом существующие моки и сценарии.

И хотелось делать все с помощью одного инструмента.

Mockingbird

Mockingbird — open-source-инструмент для мокирования, созданный Даниилом Смирновым, который работал архитектором в нашей компании. 

До августа 2023 года работа велась в репозитории корпоративного аккаунта. К сожалению, аккаунт переведен в статус Archived, поэтому развитие сервиса происходит в рамках форка. 

Инструмент умеет делать эмуляции:

  • HTTP-сервисов;

  • шинных сервисов;

  • gRPC-сервисов.

Для решения проблемы с приоритизацией моков есть механизм конфигурации — поле Scope в заглушке. В зависимости от выбранного типа Scope заглушка будет иметь меньший или больший приоритет при поступлении запроса на URL, указанный в заглушке. 

Таблица с видами Scope, их приоритетами и областью применения

Scope

Persistent

Ephemeral

Countdown

Приоритет

Наименьший

Средний

Наивысший

Автоматическое удаление

Нет

Да, через неделю

Да, каждую ночь

Применение

Обеспечение автономности тестового контура

Временное изменение постоянного (persistent) мока

Конкретный тестовый сценарий

С помощью этого механизма можно создать два мока с разным поведением для одного endpoint: один мок — с конфигурацией Persistent для Happy Path, и мок с конфигурацией Countdown, который будет реализовывать другой сценарий.

Mockingbird позволяет создавать моки двумя способами:

  • используя API;

  • через UI.

Рассмотрим механизмы инструмента при работе с HTTP-сервисами и брокерами сообщений.

Эмуляция REST-сервисов 

Инструмент валидирует тело поступившего на заглушку запроса в режимах:

  • no_body — запрос должен быть без тела;

  • any_body — тело запроса должно быть не пустым, при этом оно никак не парсится и не проверяется;

  • raw — тело запроса не парсится, проверяется на полное совпадение с описанным в моке телом запроса;

  • json — тело запроса должно быть валидным JSON и проверяется на строгое соответствие с описанным в моке json;

  • xml — тело запроса должно быть валидным XML и проверяется на строгое соответствие с описанным в моке xml; 

  • jlens — тело запроса должно быть валидным JSON и валидируется по условиям, описанным в моке, не требуя полного соответствия;

  • xpath — тело запроса должно быть валидным XML и валидируется по условиям, описанным в моке, не требуя полного соответствия;

  • web_form — тело запроса должно быть в формате x-www-form-urlencoded, валидируется по условиям, описанным в моке;

  • multipart — тело запроса должно быть в формате multipart/form-data format. Такой тип моков специфичен, об этом можно почитать в Readme.

Можно сравнивать параметры запроса на равенство и неравенство, больше или меньше числа, соответствие регулярному выражению, проверять длину значения и существование поля.

Mockingbird поддерживает режимы ответа: raw, json, xml, binary, proxy, json-proxy, xml-proxy. Режимы запроса и ответа независимы, то есть заглушка может ожидать JSON, а отвечать в формате XML.

Рассмотрим примеры несложных REST-заглушек. Здесь будет полезен список обязательных полей в HTTP-моке:

  • Name — название заглушки;

  • Method — HTTP-метод, используемый в запросе;

  • Path — эндпойнт, который хотим замокировать;

  • Scope — конфигурация (persistent/ephemeral/countdown);

  • блок Request — как валидировать запрос;

  • блок Response — как отвечать на запрос;

  • Mode — режим валидации запроса (если находится в блоке Request) и режим ответа (если находится в блоке Response);

  • Headers — заголовки запроса и ответа;

  • Delay — задержка отдачи ответа (до 30 секунд).

Пример 1. Валидация по телу запроса. Для создания мока, который срабатывает на определенное тело, нужно описать условие в блоке request. Пример реализует следующую логику: если запрос, поступивший на эндпойнт с окончанием /stubBody, имеет в теле {“id”: 42}, сработает заглушка. Она отправит статус-код 200 и тело {“field”: “Hello from body trigger mock!”}.

В моке используется режим нестрогого соответствия для json — jlens, поэтому для выполнения мока достаточно наличия поля ID со значением 42 в теле запроса. Если бы был выставлен режим строгого соответствия, json, заглушка сработала бы только в случае, если тело запроса было бы строго равно {“id”: 42}.

Пример 2. Валидация запроса по query-параметру. Отличие от валидации по телу запроса только в условии срабатывания заглушки. Она срабатывает, если значение query-параметра data, равного 1234, прописано в блоке Query, а не Body и выбран режим No_body, так как мы ожидаем запрос без тела. В ответе мы используем значение поступившего query-параметра, используя конструкцию $query: заглушка вернет имя параметра и его значение.

Пример 3. Проксирующий мок. Если не хватает тестовых контуров, хочется иметь возможность тестировать и на реальной интеграции, и на моках. Тогда можно создать мок с режимом Proxy: проксирующий мок перенаправляет запрос на другой ресурс. Это поможет не терять на тестовом контуре работу с живой интеграцией. А в случае, когда хочется работать с моком вместо нее, можно создать мок с Scope = Countdown для наивысшего приоритета при выборе подходящей заглушки. Схематично это будет выглядеть примерно так.

Эмуляция шинных сервисов

Mockingbird взаимодействует с брокерами сообщений через HTTP API, благодаря чему теоретически поддерживаются любые возможные MQ.

Наши QA-инженеры работали с RabbitMQ, IBM MQ, Kafka.

Для работы с очередями должна существовать очередь в MQ. Mockingbird читает и пишет в нее, используя, как было сказано выше, HTTP API.

Поддерживаются следующие режимы для валидации входящего сообщения:

  • raw

  • jlens

  • json

  • xml

  • xpath

И следующие форматы записи сообщения:

  • json

  • xml 

  • raw

Пример сценария для работы с шинным сервисом. Если в очередь, указанную в source (in_queue), поступит сообщение в формате JSON и с полем innerData.abc со значением test, в очередь, указанную в поле destination (out_queue), отправится сообщение {“xyz”: “abc”} в формате JSON.

Callback

Если нужно сделать цепочку вызовов — например, сначала получить ответ REST-сервиса, а затем послать сообщение в Kafka или отправить запрос по HTTP, — пригодится механизм Callback. Он позволяет выполнить дополнительное действие после основного.

Поведение после получения ответа заглушки описывается в блоке Callback. Тип Callback регулируется полем Type, которое может принимать два значения: HTTP и Message — для работы по HTTP или с брокером сообщений соответственно.

Пример с Callback по HTTP. Помимо основных полей заглушки в блоке Callback нужно указать, на какой URL обратиться, метод, тело запроса и заголовки. 

Пример с Callback в брокер сообщений. В блоке Callback указан тип = Message, очередь, куда нужно положить сообщение (поле Destination), и само сообщение в блоке Payload.

Генерация данных

Иногда нужно сгенерировать случайное значение и сохранить или вернуть его в результате работы мока. Это можно сделать с помощью блока Seed, положив туда сгенерированную строку /int/long/uuid/date/dateTime.

Для генерации данных используется блок Seed. Для определенного типа данных используется соответствующее указание. Например, для создания случайной строки длиной 20 символов будет использоваться команда %{randomString(20)}, а сгенерированное значение присваивается полю SomeId. 

Далее сгенерированные значения можно использовать через "$seed.названиеПоля", например "${seed.someId}".

Состояния, state

Есть сценарии, когда требуется моделировать разные ответы при запросе на один и тот же URL. Для решения подобных ситуаций в Mockingbird используется состояние, или State. Состояние — документ в MongoDB с данными, которые используются в качестве условия для выполнения заглушки.

В описании состояния могут присутствовать два блока: State и Persist. State отвечает за чтение, а Persist — за создание и обновление состояния.

Логика блоков:

  • если есть блок Persist, но нет блока State, создаем новое состояние;

  • если есть блок Persist и блок State, обновляем состояние, которое указали в блоке State.

Рассмотрим сценарий: эндпоинт принимает на вход ID карты и возвращает статус готовности: «Одобрено», «Подтверждено» и «Выдано».

Нужно создать три мока. Первый будет отвечать статус-кодом 200 и статусом «Одобрено», при этом создаст состояние с помощью блока Persist. В состоянии будет поле _cardId со значением номера карты в запросе и поле Status со значением Approved.

Второй мок должен вернуть статус «Подтверждено». В блоке State ищется состояние с _cardId, который совпадает с ID карты из запроса и статусом Approved. Второй мок будет отвечать статус-кодом 200 и статусом «Подтверждено». Блок Persist обновит состояние: поле Status изменится с Approved на Confirmed.

Третий мок должен вернуть статус «Выдано». Логика идентична второму моку: поиск State с номером карты, который совпадает с картой из запроса, и Status со значением Confirmed. Блок Persist обновит состояние: в поле Status значение изменится на Issued и отправится статус-код 200 со статусом «Выдано». 

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

Какие заглушки и для чего мы создаем

Для ручного тестирования Mockingbird чаще всего используется для создания:

  • вечных (scope = persistent) HTTP-моков; 

  • вечных сценариев для MQ.

Это позволяет иметь полностью замокированный тестовый контур, на котором можно сэмулировать любой тестовый сценарий. Бизнес-заказчик может пройти основной user-story фичи и проверить ее реализацию.

Для автоматизированного тестирования чаще всего используются такие функции:

  • создание одноразовых (Scope = Countdown) HTTP-моков;

  • создание одноразовых сценариев для MQ.

Заглушка или сценарий создается под конкретный автотест во время прогона, используя API Mockingbird, и удаляются после использования.
____________________________________

Применение Mockingbird помогло нам:

  • Решить проблему мокирования работы с шинными сервисами.

  • Создать стабильный контур, на котором можно пройти Happy Path и другие тестовые случаи. И все это с помощью одного инструмента.

  • Реализовать сложные связанные сценарии.

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

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

А пользуетесь ли вы инструментами для мокирования, и если да, то какими?

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


  1. xztv
    11.09.2024 11:20

    Спасибо за отличную статью!

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

    И второй вопрос: ваша команда ходит в сервис N. И соседняя команда тоже использует сервис N. В данном случае каждая команда поддерживает моки самостоятельно, или есть какой-то механизм шеринга?