Как мы сделали grpc-wiremock: сервис, создающий мок-сервер для ваших контрактов в одну команду
- четверг, 8 июня 2023 г. в 00:00:25
Всем привет, меня зовут Никита, уже пару лет я зарабатываю на жизнь развитием платформы-как-сервис в СберМаркет. В основном я отвечаю за инструменты локальной разработки и являюсь одним из создателей cli, которая позволяет развернуть сервис со всеми его зависимостями в одну команду sbm-cli service up
.
Если взглянуть на топ-50 вакансии backend-разработчиков, станет очевидно, что в большинстве из них требуется знание микросервисной архитектуры. Это же касается и QA. Не переходя к привычному обсуждению плюсов и минусов данного подхода, предлагаю обсудить тестирование в МСА. Если конкретнее, тестирование на моках и то, как мы это делаем в PaaS.
Спойлер: мы написали своё решение на основе Wiremock и совсем недавно сделали его опенсорсным. Призываю пользоваться и задавать вопросы, но обо всём по порядку.
Существует несколько типов тестирования в микросервисной архитектуре:
модульное тестирование предназначено для проверки отдельных компонентов приложения
интеграционное тестирование позволяет проверить взаимодействие между компонентами
тестирование контрактов позволяет гарантировать, что все компоненты приложения соблюдают заранее определенные контракты
Прошу заметить, речи о системном (или E2E тестировании) здесь не идет. Наличие нескольких единиц развертывания приводит к ненадежности тестового окружения, а значит тесты написанные на «все везде и сразу» приведут к высоким издержкам не только в плане поддержки, но и с точки зрения счетов за облачную инфраструктуру. Альтернативой является интеграционное тестирование с использованием мок-сервера.
Тестирование с помощью мок-сервера позволяет создавать имитацию внешних сервисов и API. Мок-сервер может быть использован для тестирования приложений, которые используют внешние сервисы, например, социальные сети, платежные системы, почтовые сервисы и т.д.
Мок-сервер позволяет создавать тестовые сценарии и проверять работу приложения в различных условиях. Это особенно полезно при разработке больших проектов, когда необходимо тестировать множество взаимодействий с внешними сервисами.
Вот преимущества использования мок-сервера для тестирования:
тестирование приложения в изолированной среде
повторное использование моков для других проектов
ускорение процесса разработки и тестирования приложения
Мысленный эксперимент. Случайный читатель обнаруживает себя на месте разработчика или тестировщика сервиса X, который зависит от сервисов Y и Z.
...то, вероятно, вы сгенерируете моки прямо в коде. Для вас это привычная задача и скорее всего запуск отдельного контейнера с мок-сервером вас не заинтересует. Да и правило хорошего тона — много юнит тестов.
Но, что если вам достался комок легаси кода? И для того чтобы создавать моки, вам нужно внести уйму правок в код? В такой ситуации поднятие отдельного контейнера с мок-сервером не худшая затея. К тому же, как я уже говорил в начале, у нас есть cli, которая запускает сервис со всеми его зависимостями в одну команду. Остается только сделать так, чтобы развернуть контейнер с моками было проще, чем написать/сгенерировать моки в коде.
...все гораздо проще. Когда вам нужно протестировать связку сервисов, то вариант с написанием моков в коде отпадает по понятным причинам. Более того, если ваш сервис зависит от внешних поставщиков API, которые нельзя развернуть на стейдже, у вас остается чуть ли не единственный вариант — запускать мок-сервер.
Результатом мысленного эксперимента в нашей команде стало решение встроить в нашу инфраструктуру для локальной разработки инструмент. На основе OpenAPI и Proto-контрактов он автоматически сгенерирует моки, запустит мок-сервер и позволит бесшовно работать с API сервиса так, будто рядом запущена его реальная копия. Очевидно, что это сделает проще жизнь и разработчика, и QA инженера. А большего нам и не надо.
Итак, настало время выбрать мок-сервер из существующих решений.
Технические возможности:
поддержка gRPC
поддержка TLS/SSL
возможность запуска сразу нескольких мок API
возможность развернуть сервер как stand-alone приложение
Функционал:
запуск в режиме proxy
поддержка callback’ов в моках
возможность имитировать задержки и сетевые сбои
возможность гибко настраивать моки (в том числе динамически)
Простота использования:
единый способ конфигурации для HTTP и gRPC
наличие autoreload (при изменении контрактов или моков)
простое создание и настройка моков (без знания какого-либо языка программирования)
Автотестирование:
наличие API
скорость запуска контейнера
Как можно заметить, при выборе мы очень внимательно отнеслись к простоте использования решения. Нам важно сохранить низкий порог входа для людей без опыта в программировании.
Если вкратце, то найти технологию, которая удовлетворяла бы всем нашим запросам, оказалось невозможно. В тот же момент пришло осознание, что придется готовить свое решение, которое вберёт в себя лучшие наработки и закроет все наши потребности. Одним из самых продвинутых и популярных вариантов оказался Wiremock. Он и был взят за основу. Главный его минус (отсутствие поддержки gRPC) решили устранить самостоятельно.
Здесь можно более подробно ознакомиться с существующими проектами.
Wiremock позволяет создавать моки для HTTP и HTTPS протоколов. Он поддерживает различные типы запросов, включая GET, POST, PUT, DELETE и другие. А еще имеет режим Record/Playback и имитацию сетевых задержек.
Если есть желание узнать подробнее о возможностях Wiremock, можно почитать документацию. К тому же, на Хабре уже есть исчерпывающая статья о возможностях настройки Wiremock.
Приступим к реализации.
Ранее упоминалось, что найти идеальное решение не удалось. Предлагаю обозначить все доработки необходимые для релиза grpc-wiremock, а потом разобраться в деталях реализации каждого компонента.
Что было решено доделывать самостоятельно?
поддержка gRPC
роутинг по домену (позволит не меняя настроек окружения получать доступ к нужному мок-API)
отслеживание изменений в контрактах, доменах, моках (позволит не перезапускать контейнер для применения изменений)
поддержка MultiAPI
поддержка TLS/SSL для HTTP и gRPC
Поддержку gRPC решено было сделать с помощью прокси сервера. Прокси основан на proto контрактах и представляет собой проект на Go, который после запуска начинает принимать gRPC запросы, конвертировать их в HTTP и проксировать на конкретный порт мок-API в Wiremock.
За генерацию прокси сервера отвечает утилита grpc2http.
Так выглядит директория с контрактами:
deps
└── services
└── push-sender
└── grpc
└── push-sender.proto
Пример proto контракта push-sender.proto
:
syntax = "proto3";
package push_sender;
...
service PushSender {
rpc Notify(NotifyRequest) returns (NotifyResponse);
}
message NotifyRequest {
string uuid = 1;
string message = 2;
}
message NotifyResponse {
uint32 status = 1;
}
Пример генерации и запуска grpc-to-http-proxy:
WIREMOCK_DOMAIN_API_ADDR="http://localhost:8000"
DEPENDENCY_CONTRACTS="deps/services/awesome-service/proto"
grpc2http \
--input "${DEPENDENCY_CONTRACTS}" \
--base-url "${DOMAIN_API_ADDR}" \
--output "/tmp/ready-to-run-proxy"
make run -C "/tmp/ready-to-run-proxy"
Прокси поддерживает все типы gRPC взаимодействия, включая стримы. Если вам интересно, как это работает под капотом, посмотреть примеры можно здесь.
Режим поддержки нескольких API необходим, когда нужно тестировать несколько микросервисов сразу.
Чтобы поддержать такой режим, мы взяли за основу инструмент, который позволяет на основе конфигурационных файлов запускать или останавливать отдельные процессы с экземплярами Wiremock.
Для того чтобы добавить новый домен (экземпляр Wiremock) в контейнере, нужно в директорию deps/services
добавить контракты сервиса-зависимости и выполнить команду:
docker exec grpc-wiremock reload
После её выполнения новый экземпляр API будет доступен на порте +1 от предыдущего экземпляра. Нумерация портов начинается с 8000.
В принципе, на данный момент grpc-wiremock уже работоспособен. Осталось добавить поддержку TLS/SSL, а также роутинг по названию домена. Роутинг поможет не менять настройки окружения проекта при переходе на мок-сервер.
Для этих целей мы используем NGINX. При запуске grpc-wiremock происходит генерация сертификатов и NGINX конфигов для каждого из доменов.
Схема для HTTP запросов:
Схема для gRPC запросов:
Пример .conf
файла для сервиса-зависимости push-sender
:
server {
server_name push-sender push-sender.*;
listen 80;
listen [::]:80;
listen 443 ssl;
ssl_certificate /etc/ssl/mock/mock.crt;
ssl_certificate_key /etc/ssl/mock/mock.key;
access_log /var/log/nginx/push-sender.access.log main;
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://localhost:8000;
}
}
Добавление такого конфигурационного файла NGINX позволяет по имени домена перенаправлять запросы пользователя на конкретный порт мок-сервера.
В случае push-sender
, запрос вида curl push-sender
будет перенаправлен на http://localhost:8000
.
При создании следующего домена добавится мок-API с портом :8001
и подобный файл для NGINX.
В случае обращения по имени домена push-sender-new
, который не существует на данный момент, в логах контейнера появится сообщение.
gw.routing.nginx.logs: warn: domain "push-sender-new" is not mocked ..."
Рекомендуется добавить имя домена в /etc/hosts
, в таком случае не придется добавлять специальные заголовки к HTTP и gRPC запросам, которые понадобятся для роутинга с помощью NGINX.
echo '127.0.0.1 push-sender' | sudo tee -a "/etc/hosts"
Впрочем, можно обращаться к grpc-wiremock и без изменения /etc/hosts
.
Примеры запросов:
curl -XPOST --header "Host: push-sender" localhost
{
"status" : 425895108
}
grpcurl \
--authority "push-sender" \
...
localhost push_sender.PushSender/Notify
{
"status": 425895108
}
Для того чтобы grpc-wiremock работал с сервисами, использующими TLS/SSL для обращения к HTTP и gRPC-ручкам, необходимо добавить сертификат, который будет считаться доверенным на хосте пользователя.
При первом запуске grpc-wiremock будет сгенерирован Certificate Authority/CA. Он будет использован для выпуска сертификата внутри grpc-wiremock контейнера. К тому же CA будет доступен пользователю (через примонтированный docker volume). Это даст возможность пользователю выпускать дополнительные сертификаты на основе предоставленного CA.
Команда для добавления сертификата в системное хранилище на хосте:
sudo cp $(pwd)/certs/mockCA.crt /etc/ssl/certs && update-ca-certificates --fresh
Что касается настроек внутри контейнера, то сгенерированные сертификаты используются для того, чтобы NGINX получил возможность проксировать защищенный трафик. Для внешнего наблюдателя все выглядит так, будто мок-сервер работает с поддержкой TLS/SSL, однако Wiremock получает только plain HTTP/gRPC запросы.
На данный момент grpc-wiremock уже почти полностью соответствует нашим требованиям. Снова есть «но».
Если вы изменили контракты, моки или директории с доменами, придется перезапускать контейнер для того, чтобы изменения вступили в силу. Это никуда не годится. Для того чтобы автоматизировать процесс применения изменений, мы написали watcher событий файловой системы с помощью библиотеки rfsnotify.
Последняя доработка позволит на лету изменять артефакты, сразу видеть результат изменений, а значит — быстрее разрабатывать и тестировать ваши сервисы.
Теперь все требования к нашему мок-серверу удовлетворены. Значит, мы можем перейти к схеме grpc-wiremock в сборе, а затем обсудить пользовательский интерфейс.
На схеме собраны все элементы grpc-wiremock, о которых шла речь выше.
Для старта grpc-wiremock вам потребуются 3 директории.
1) Директория deps
. Содержит контракты сервисов-зависимостей. Схема хранения контрактов во вложенных директориях важна. Proto и OpenAPI.
tree deps
deps
└── services
└── push-sender
└── grpc
└── push-sender.proto
2) Директория для Wiremock конфига и моков. test/wiremock
. При первом запуске может быть пустой.
tree test/wiremock
test/wiremock
└── push-sender
├── __files
└── mappings
3) Директория для сертификатов. Изначально — пуста, при старте сюда будет сохранен Certifiacte Authority.
tree certs
certs/
└── mockCA.crt
Команда для запуска контейнера grpc-wiremock:
MOCKS_PATH="$(PWD)/test/wiremock"
CONTRACTS_PATH="$(PWD)/deps"
CERTS_PATH="$(PWD)/certs"
WIREMOCK_GUI_PORT=9000
NGINX_PORT=80
docker run \
-it --rm --name grpc-wiremock \
-p ${WIREMOCK_GUI_PORT}:${WIREMOCK_GUI_PORT} \
-p ${NGINX_PORT}:${NGINX_PORT} \
-v ${MOCKS_PATH}:/home/mock \
-v ${CERTS_PATH}:/etc/ssl/mock/share \
-v ${CONTRACTS_PATH}:/contracts \
sbermarkettech/grpc-wiremock:latest
Из контейнера с приложением:
grpcurl \
-d '{"uuid": "1234", "message": "foo"}' --plaintext \
push-sender push_sender.PushSender/Notify
{
"status": 425895108
}
curl -XPOST push-sender/PushSender/Notify
{
"status" : 425895108
}
C хоста пользователя:
echo '127.0.0.1 push-sender' | sudo tee -a "/etc/hosts"
grpcurl \
-d '{"uuid": "1234", "message": "foo"}' --plaintext \
push-sender push_sender.PushSender/Notify
Так же, вы можете обратить внимание на наш репозиторий с тестовым сервисом и настроенным для него Wiremock.
Использование тестов с мок-сервером в микросервисной архитектуре позволяет тестировать каждый сервис в отдельности. Это существенно ускоряет процесс разработки и повышает качество кода. Однако, необходимо учитывать, что использование мок-тестов не заменяет полноценного интеграционного тестирования, которое необходимо для проверки взаимодействия между сервисами и подтверждения их корректной работы в совокупности.
Как видно из описания деталей реализации, мы проделали большую работу. И все для того, чтобы тестировать сервисы стало проще. Мы будем рады, если наше решение подойдет и для вас. А еще, можно приносить идеи по развитию проекта, более того, можно развивать проект самостоятельно. Мы всегда рады вашим Merge Request'ам.
Приносить идеи и Merge Requst'ы — сюда. Спасибо за внимание!
Ссылки на проект:
Использованные технологии:
Дополнительно про Wiremock:
Tech-команда СберМаркета ведет соцсети с новостями и анонсами. Если хочешь узнать, что под капотом высоконагруженного e-commerce, следи за нами в Telegram и на YouTube. А также слушай подкаст «Для tech и этих» от наших it-менеджеров.