Привет! Меня зовут Ольга Инеева, я ведущий инженер по обеспечению качества в Т-Банке. Расскажу о проблемах тестирования интеграции и об инструменте для мокирования Mockingbird. Мы решили проблему сложных связанных сценариев и хотим поделиться этим знанием. Добро пожаловать под кат!
Проблемы тестирования интеграций
Наше подразделение занимается продуктами, связанными с BNPL-политикой (buy now, pay later). Это такие продукты, как автокредит, ипотека, сервис «Долями», потребительские кредиты и другое. У нас много интеграций — как внутрибанковских, так и внешних. На стабильность работы внешних систем мы чаще всего не можем повлиять.
Мы столкнулись со множеством проблем при тестировании этих интеграций:
Сторонняя система еще не реализована, а мы уже хотим протестировать взаимодействие с ней.
Тестовый контур приложения, с которым мы интегрируемся, работает нестабильно.
Долгий ответ внешнего сервиса. Можно отвлечься на другие задачи, пока ждешь ответа, а писать автотесты через Thread.sleep(N) — не лучшая идея.
Сложность в получении ответа, для которого нужны специфические данные.
Необходимость ручных действий соседней команды. Неудобно постоянно просить коллегу нажать кнопку или прописать данные.
Нехватка тестовых контуров. Бывают случаи, когда тестовый контур у системы отсутствует, или каждый поход в нее платный, что сильно сокращает возможности тестирования.
В подобных случаях лучше использовать моки. Моки, или заглушки, –— это эмуляция сервиса, с которым у нас есть интеграция. Главное правило: не увлечься ими, протестировать в рамках e2e-сценариев и живой интеграции, иначе рискуем словить непредвиденный баг.
Плюсы мокирования:
Тесты становятся быстрее. Интеграции на тестовых контурах отвечают с задержкой, а развернутый мок-сервер — практически мгновенно.
Возможно сэмулировать все тестовые случаи.
Растет уровень доверия к тестам.
Минусы:
Риск словить баг на реальной интеграции.
Необходимо поддерживать моки в актуальном состоянии. Можно попасть в ситуацию, когда другой сервис обновил контракт в одностороннем порядке, а тесты остались «зелеными». От этого нас могут защитить контрактные тесты, но это уже совсем другая история.
Уровень доверия к тестам
Представьте, что в ваших тестах используется реальная интеграция, а не моки. Вы смотрите утром отчет по ночному прогону, и он выглядит так:
Вы думаете о том, почему упали 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 и другие тестовые случаи. И все это с помощью одного инструмента.
Реализовать сложные связанные сценарии.
В результате команда тестирования легко управляется с актуализацией тестовых сценариев, не переживает из-за флакающих тестов и нестабильности окружающей инфраструктуры.
Буду рада, если мой материал поможет в улучшении стабильности тестового окружения. Надеюсь, что сэкономленное время будет использоваться для улучшения процессов вместо выяснения ложных падений тестов.
А пользуетесь ли вы инструментами для мокирования, и если да, то какими?
xztv
Спасибо за отличную статью!
Вопрос: а как вы поддерживаете актуальность моков в соответствии с контрактами внешних сервисов? Пока у вас интеграция с одним-двумя сервисами, вроде все легко, но когда число интеграций увеличивается - число моков, которые необходимо поддерживать растет в прогрессии.
И второй вопрос: ваша команда ходит в сервис N. И соседняя команда тоже использует сервис N. В данном случае каждая команда поддерживает моки самостоятельно, или есть какой-то механизм шеринга?
olphena Автор
Спасибо за вопросы!
>как вы поддерживаете актуальность моков в соответствии с контрактами внешних сервисов?
Если коротко - руками. Да, количество моков, которые нужно поддерживать, и правда немаленькое, но любые изменения во взаимодействии с внешним сервисом отслеживаются, ставится задача на изменение контракта. А в рамках задачи на тестирование изменений QA в том числе правит моки - вечные для ручного тестирования и шаблоны заглушек для автотестов.
>ваша команда ходит в сервис N. И соседняя команда тоже использует сервис N. В данном случае каждая команда поддерживает моки самостоятельно, или есть какой-то механизм шеринга?
Каждая команда поддерживает моки самостоятельно.