golang

Как мы сделали grpc-wiremock: сервис, создающий мок-сервер для ваших контрактов в одну команду

  • четверг, 8 июня 2023 г. в 00:00:25
https://habr.com/ru/companies/sbermarket/articles/739968/

Всем привет, меня зовут Никита, уже пару лет я зарабатываю на жизнь развитием платформы-как-сервис в СберМаркет. В основном я отвечаю за инструменты локальной разработки и являюсь одним из создателей cli, которая позволяет развернуть сервис со всеми его зависимостями в одну команду sbm-cli service up.

Если взглянуть на топ-50 вакансии backend-разработчиков, станет очевидно, что в большинстве из них требуется знание микросервисной архитектуры. Это же касается и QA. Не переходя к привычному обсуждению плюсов и минусов данного подхода, предлагаю обсудить тестирование в МСА. Если конкретнее, тестирование на моках и то, как мы это делаем в PaaS.

Спойлер: мы написали своё решение на основе Wiremock и совсем недавно сделали его опенсорсным. Призываю пользоваться и задавать вопросы, но обо всём по порядку.

Тестирование микросервисной архитектуры

Существует несколько типов тестирования в микросервисной архитектуре:

  • модульное тестирование предназначено для проверки отдельных компонентов приложения

  • интеграционное тестирование позволяет проверить взаимодействие между компонентами

  • тестирование контрактов позволяет гарантировать, что все компоненты приложения соблюдают заранее определенные контракты

Прошу заметить, речи о системном (или E2E тестировании) здесь не идет. Наличие нескольких единиц развертывания приводит к ненадежности тестового окружения, а значит тесты написанные на «все везде и сразу» приведут к высоким издержкам не только в плане поддержки, но и с точки зрения счетов за облачную инфраструктуру. Альтернативой является интеграционное тестирование с использованием мок-сервера.

Тестирование с помощью мок-сервера позволяет создавать имитацию внешних сервисов и API. Мок-сервер может быть использован для тестирования приложений, которые используют внешние сервисы, например, социальные сети, платежные системы, почтовые сервисы и т.д.

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

Вот преимущества использования мок-сервера для тестирования:

  • тестирование приложения в изолированной среде

  • повторное использование моков для других проектов

  • ускорение процесса разработки и тестирования приложения

А зачем поднимать отдельный мок-сервер?

Мысленный эксперимент. Случайный читатель обнаруживает себя на месте разработчика или тестировщика сервиса X, который зависит от сервисов Y и Z.

Если вы разработчик...

...то, вероятно, вы сгенерируете моки прямо в коде. Для вас это привычная задача и скорее всего запуск отдельного контейнера с мок-сервером вас не заинтересует. Да и правило хорошего тона — много юнит тестов.

Но, что если вам достался комок легаси кода? И для того чтобы создавать моки, вам нужно внести уйму правок в код? В такой ситуации поднятие отдельного контейнера с мок-сервером не худшая затея. К тому же, как я уже говорил в начале, у нас есть cli, которая запускает сервис со всеми его зависимостями в одну команду. Остается только сделать так, чтобы развернуть контейнер с моками было проще, чем написать/сгенерировать моки в коде.

Если вы QA-инженер...

...все гораздо проще. Когда вам нужно протестировать связку сервисов, то вариант с написанием моков в коде отпадает по понятным причинам. Более того, если ваш сервис зависит от внешних поставщиков API, которые нельзя развернуть на стейдже, у вас остается чуть ли не единственный вариант — запускать мок-сервер.

Результатом мысленного эксперимента в нашей команде стало решение встроить в нашу инфраструктуру для локальной разработки инструмент. На основе OpenAPI и Proto-контрактов он автоматически сгенерирует моки, запустит мок-сервер и позволит бесшовно работать с API сервиса так, будто рядом запущена его реальная копия. Очевидно, что это сделает проще жизнь и разработчика, и QA инженера. А большего нам и не надо.

Итак, настало время выбрать мок-сервер из существующих решений.

Выбор мок-сервера

  • Технические возможности:

    • поддержка gRPC

    • поддержка TLS/SSL

    • возможность запуска сразу нескольких мок API

    • возможность развернуть сервер как stand-alone приложение

  • Функционал:

    • запуск в режиме proxy

    • поддержка callback’ов в моках

    • возможность имитировать задержки и сетевые сбои

    • возможность гибко настраивать моки (в том числе динамически)

  • Простота использования:

    • единый способ конфигурации для HTTP и gRPC

    • наличие autoreload (при изменении контрактов или моков)

    • простое создание и настройка моков (без знания какого-либо языка программирования)

  • Автотестирование:

    • наличие API

    • скорость запуска контейнера

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

Если вкратце, то найти технологию, которая удовлетворяла бы всем нашим запросам, оказалось невозможно. В тот же момент пришло осознание, что придется готовить свое решение, которое вберёт в себя лучшие наработки и закроет все наши потребности. Одним из самых продвинутых и популярных вариантов оказался Wiremock. Он и был взят за основу. Главный его минус (отсутствие поддержки gRPC) решили устранить самостоятельно.

Здесь можно более подробно ознакомиться с существующими проектами.

Wiremock

Wiremock позволяет создавать моки для HTTP и HTTPS протоколов. Он поддерживает различные типы запросов, включая GET, POST, PUT, DELETE и другие. А еще имеет режим Record/Playback и имитацию сетевых задержек.

Если есть желание узнать подробнее о возможностях Wiremock, можно почитать документацию. К тому же, на Хабре уже есть исчерпывающая статья о возможностях настройки Wiremock.

Приступим к реализации.

Что под капотом у grpc-wiremock?

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

Что было решено доделывать самостоятельно?

  • поддержка gRPC

  • роутинг по домену (позволит не меняя настроек окружения получать доступ к нужному мок-API)

  • отслеживание изменений в контрактах, доменах, моках (позволит не перезапускать контейнер для применения изменений)

  • поддержка MultiAPI

  • поддержка TLS/SSL для HTTP и gRPC

Поддержка 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 взаимодействия, включая стримы. Если вам интересно, как это работает под капотом, посмотреть примеры можно здесь.

Поддержка MultiAPI

Режим поддержки нескольких 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, о которых шла речь выше.

Как использовать?

Для старта 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-менеджеров.