Блог

· обновлено

Как тестировать gRPC-сервисы

Тестирование gRPC начинается не с JSON и URL, а со схемы. Клиенту нужно знать сервис, метод и protobuf-типы, иначе он не сможет закодировать сообщение. Когда схема загружена, работа напоминает обычную проверку API: вы задаёте адрес, metadata и тело, отправляете вызов и разбираете статус. Разница проявляется в потоках, deadline и TLS.

Ниже мы используем grpcurl для воспроизводимых команд. Те же шаги доступны в графических клиентах с поддержкой gRPC, включая Tetiva: подключить reflection или .proto, выбрать метод, заполнить сообщение и отправить вызов.

Тестовая схема

Представим сервис заказов с обычным методом и серверным потоком:

syntax = "proto3";

package shop.v1;

service OrderService {
  rpc GetOrder(GetOrderRequest) returns (Order);
  rpc WatchOrders(WatchOrdersRequest) returns (stream Order);
  rpc ImportOrders(stream CreateOrderRequest) returns (ImportSummary);
  rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}

message GetOrderRequest {
  string id = 1;
}

message WatchOrdersRequest {
  string customer_id = 1;
}

message CreateOrderRequest {
  string customer_id = 1;
  repeated string sku = 2;
}

message Order {
  string id = 1;
  string status = 2;
}

message ImportSummary {
  int32 accepted = 1;
}

message ChatMessage {
  string text = 1;
}

Полное имя unary-метода — shop.v1.OrderService/GetOrder. Пакет важен: вызов только OrderService/GetOrder часто заканчивается UNIMPLEMENTED, хотя сервер работает.

Tetiva
v0.9.7
Схема gRPC-сервиса, полученная через reflection, в TetivaСхема gRPC-сервиса, полученная через reflection, в Tetiva
Reflection подтянул схему — методы и типы уже в клиенте

Server reflection: получить схему от сервера

Server reflection позволяет клиенту запросить список сервисов и их дескрипторы во время выполнения. Для разработки это самый быстрый путь: не нужно искать совпадающую версию .proto.

Проверьте, включён ли reflection:

grpcurl -plaintext localhost:50051 list
grpcurl -plaintext localhost:50051 list shop.v1.OrderService
grpcurl -plaintext localhost:50051 describe shop.v1.OrderService.GetOrder

Если сервер отвечает, выберите метод и отправьте сообщение:

grpcurl -plaintext \
  -d '{"id":"ord_123"}' \
  localhost:50051 \
  shop.v1.OrderService/GetOrder

В production reflection часто отключают, чтобы не публиковать каталог сервисов. Тогда передайте исходные .proto и пути импортов:

grpcurl -plaintext \
  -import-path ./proto \
  -proto shop/v1/orders.proto \
  -d '{"id":"ord_123"}' \
  localhost:50051 \
  shop.v1.OrderService/GetOrder

Если схема импортирует google/type/date.proto, одного главного файла недостаточно: добавьте каталог, внутри которого начинается путь google/type/date.proto.

Как получить пример запроса из схемы

Не придумывайте JSON по названию метода. Откройте входной message в дескрипторе и заполните поля с учётом protobuf JSON mapping. Имена можно писать как customerId; многие инструменты принимают и исходное customer_id. Значения int64 в JSON обычно передаются строкой, enum — именем, bytes — строкой Base64, Timestamp — временем в RFC 3339.

Для CreateOrderRequest минимальный осмысленный пример выглядит так:

{
  "customerId": "cus_42",
  "sku": ["book-1", "pen-2"]
}

Автогенерация в API-клиенте экономит время, но результат остаётся заготовкой. Пустая строка, ноль и первый enum-элемент — допустимые значения protobuf, однако бизнес-валидацию они могут не пройти. В Tetiva пример тела строится из загруженной схемы; перед отправкой замените заглушки доменными значениями.

Unary и три вида streaming

У gRPC четыре формы вызова:

  • Unary: один запрос, один ответ. Его проще всего повторять и включать в smoke-тесты.
  • Server streaming: один запрос, несколько ответов. Проверяйте порядок событий, завершение потока и поведение при отмене клиентом.
  • Client streaming: несколько запросов, один итоговый ответ. Важно явно закрыть отправляющую сторону, иначе сервер продолжит ждать сообщения.
  • Bidirectional streaming: обе стороны отправляют сообщения независимо. Тестируйте не только содержимое, но и последовательность: ответ не обязан соответствовать последнему отправленному сообщению один к одному.

Серверный поток в grpcurl запускается обычной командой; ответы печатаются по мере поступления:

grpcurl -plaintext \
  -d '{"customerId":"cus_42"}' \
  localhost:50051 \
  shop.v1.OrderService/WatchOrders

Для клиентского и двунаправленного потока удобнее интерактивный режим или графический клиент: откройте соединение, отправьте несколько сообщений, затем завершите клиентскую половину потока. Зафиксируйте ожидаемые события и таймаут ожидания, иначе зависший тест будет выглядеть как медленный.

Metadata и аутентификация

Metadata — это пары ключ-значение поверх HTTP/2. Ключи пишутся в нижнем регистре; бинарные ключи оканчиваются на -bin. Токен обычно передают так:

grpcurl -plaintext \
  -H 'authorization: Bearer dev-token' \
  -H 'x-request-id: test-001' \
  -d '{"id":"ord_123"}' \
  localhost:50051 \
  shop.v1.OrderService/GetOrder

Если приходит UNAUTHENTICATED, сначала проверьте наличие и формат токена. PERMISSION_DENIED чаще означает, что личность уже установлена, но у неё нет права на действие. Не сохраняйте рабочие токены прямо в коллекции: используйте локальное окружение или хранилище секретов.

Deadline: ограничить ожидание

У каждого тестового вызова должен быть deadline. Без него потерянный пакет, зависшая зависимость или незакрытый stream могут удерживать проверку неопределённо долго.

grpcurl -plaintext \
  -max-time 3 \
  -d '{"id":"ord_123"}' \
  localhost:50051 \
  shop.v1.OrderService/GetOrder

Статус DEADLINE_EXCEEDED не доказывает, что сервер ничего не сделал: операция могла завершиться после того, как клиент перестал ждать. Для изменяющих запросов используйте идемпотентный ключ и проверяйте серверные логи по request ID. Слишком короткий deadline создаёт нестабильные тесты, слишком длинный скрывает деградацию; выбирайте предел из бюджета конкретной операции.

TLS, mTLS и частые ошибки адреса

Для локального сервера без TLS нужен -plaintext. Для защищённого адреса флаг убирают:

grpcurl api.example.com:443 list

С собственным центром сертификации передайте CA, а для mTLS — клиентский сертификат и ключ:

grpcurl \
  -cacert ./certs/ca.pem \
  -cert ./certs/client.pem \
  -key ./certs/client-key.pem \
  api.example.com:443 list

Ошибка certificate is valid for ... указывает на несовпадение имени хоста в сертификате. Подключение по IP вместо DNS-имени также меняет TLS SNI и маршрутизацию прокси. Отключать проверку сертификата допустимо лишь для локальной диагностики: такой обход не должен попадать в сохранённый production-профиль.

Минимальный чек-лист

Сначала подтвердите адрес и режим TLS, затем загрузите схему через reflection или .proto. Проверьте полное имя метода, сгенерируйте тело по message-типу, добавьте metadata и разумный deadline. Для streaming отдельно протестируйте отправку, получение, закрытие и отмену. Наконец, сохраните удачный вызов как коллекционный тест — так ручная находка станет повторяемой проверкой.