Как читать gRPC контракт.

Как читать gRPC контракт.

Alexey Klimenko

gRPC-контракт - это формальное описание того, как один сервис может обращаться к другому.

В REST API мы обычно смотрим на HTTP-метод, URL, query-параметры, тело запроса и тело ответа. В gRPC основной точкой входа становится .proto-файл. В нём описано, какие методы предоставляет сервис, какие сообщения он принимает, какие ответы возвращает и какие типы данных используются.

Для тестировщика gRPC-контракт выполняет примерно ту же роль, что Swagger/OpenAPI для REST API. Он помогает понять структуру интеграции ещё до того, как мы начали отправлять реальные запросы. По контракту можно увидеть доступные операции, состав входных и выходных данных, enum-значения, списки, optional-поля, потоковые методы и потенциальные зоны для проверок.

Но для этого контракт нужно научиться читать и понимать.


Разберём пример.

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

Но команде понадобился отдельный аналитический сервис. Он не должен дублировать основное API. Его задача: считать агрегированные показатели. Например, сколько курсов пользователь завершил, какой у него процент завершения, как он выглядит на фоне среднего значения по платформе, в какой сегмент попадает и какие события аналитики обновились.

Такой сервис может использоваться внутренней админкой, продуктовой аналитикой или рекомендательным механизмом. Основной сервис платформы может обращаться к нему по gRPC.

Контракт может выглядеть так:

syntax = "proto3";

package education.analytics.v1;

import "google/protobuf/timestamp.proto";

service LearningAnalyticsService {
  rpc GetUserProgressAnalytics(GetUserProgressAnalyticsRequest) returns (UserProgressAnalyticsResponse);

  rpc CompareUserWithPlatform(CompareUserWithPlatformRequest) returns (UserPlatformComparisonResponse);

  rpc ListTopUsers(ListTopUsersRequest) returns (ListTopUsersResponse);

  rpc WatchAnalyticsUpdates(WatchAnalyticsUpdatesRequest) returns (stream AnalyticsUpdatedEvent);
}

message GetUserProgressAnalyticsRequest {
  string user_id = 1;
  AnalyticsPeriod period = 2;
}

message UserProgressAnalyticsResponse {
  string user_id = 1;
  int32 total_courses = 2;
  int32 completed_courses = 3;
  double completion_rate_percent = 4;
  AnalyticsPeriod period = 5;
  UserProgressSegment segment = 6;
  google.protobuf.Timestamp calculated_at = 7;
}

message CompareUserWithPlatformRequest {
  string user_id = 1;
  AnalyticsPeriod period = 2;
}

message UserPlatformComparisonResponse {
  string user_id = 1;
  double user_completion_rate_percent = 2;
  double platform_average_completion_rate_percent = 3;
  double difference_from_average_percent = 4;
  int32 user_rank = 5;
  int32 total_users_in_rating = 6;
}

message ListTopUsersRequest {
  AnalyticsPeriod period = 1;
  int32 page_size = 2;
  string page_token = 3;
}

message ListTopUsersResponse {
  repeated TopUserItem items = 1;
  string next_page_token = 2;
}

message TopUserItem {
  string user_id = 1;
  double completion_rate_percent = 2;
  int32 completed_courses = 3;
  int32 rank = 4;
}

message WatchAnalyticsUpdatesRequest {
  AnalyticsPeriod period = 1;
  optional UserProgressSegment segment = 2;
}

message AnalyticsUpdatedEvent {
  AnalyticsPeriod period = 1;
  double platform_average_completion_rate_percent = 2;
  int32 total_users = 3;
  google.protobuf.Timestamp occurred_at = 4;
}

enum AnalyticsPeriod {
  ANALYTICS_PERIOD_UNSPECIFIED = 0;
  ANALYTICS_PERIOD_LAST_30_DAYS = 1;
  ANALYTICS_PERIOD_LAST_90_DAYS = 2;
  ANALYTICS_PERIOD_ALL_TIME = 3;
}

enum UserProgressSegment {
  USER_PROGRESS_SEGMENT_UNSPECIFIED = 0;
  USER_PROGRESS_SEGMENT_NO_PROGRESS = 1;
  USER_PROGRESS_SEGMENT_LOW = 2;
  USER_PROGRESS_SEGMENT_MEDIUM = 3;
  USER_PROGRESS_SEGMENT_HIGH = 4;
}


Читать такой контракт лучше сверху вниз.

Первая строка:

syntax = "proto3";

Она указывает версию синтаксиса Protocol Buffers. Это не бизнесовая часть контракта, но она важна технически. Синтаксис влияет на правила описания сообщений, значения по умолчанию и поведение некоторых полей.

Дальше идёт:

package education.analytics.v1;

package задаёт логическую область контракта. В примере видно, что речь идёт об аналитическом сервисе образовательной платформы первой версии. Версия в названии важна, потому что контракт может развиваться: в него могут добавляться новые поля, методы и enum-значения.

Затем импортируется внешний тип:

import "google/protobuf/timestamp.proto";

Это означает, что контракт использует стандартный protobuf-тип для даты и времени. В примере он нужен для полей calculated_at и occurred_at.

После этого начинается основной блок:

service LearningAnalyticsService {
  rpc GetUserProgressAnalytics(GetUserProgressAnalyticsRequest) returns (UserProgressAnalyticsResponse);

  rpc CompareUserWithPlatform(CompareUserWithPlatformRequest) returns (UserPlatformComparisonResponse);

  rpc ListTopUsers(ListTopUsersRequest) returns (ListTopUsersResponse);

  rpc WatchAnalyticsUpdates(WatchAnalyticsUpdatesRequest) returns (stream AnalyticsUpdatedEvent);
}

service можно воспринимать как список доступных операций. В REST API мы бы искали endpoint-ы, а в gRPC смотрим на rpc-методы.

Метод GetUserProgressAnalytics
- принимает GetUserProgressAnalyticsRequest и
- возвращает UserProgressAnalyticsResponse.

Уже по этой строке понятно, что клиент отправляет запрос с параметрами пользователя и получает аналитический ответ по его прогрессу.

Метод CompareUserWithPlatform сравнивает пользователя со средними значениями платформы.

Метод ListTopUsers возвращает список пользователей с лучшим прогрессом. По названию и структуре ответа можно ожидать коллекцию данных и пагинацию.

Метод WatchAnalyticsUpdates отличается от остальных. Он возвращает stream AnalyticsUpdatedEvent. Это значит, что сервер может отправить не один ответ, а поток событий. Такой метод может использоваться, например, для внутренней панели, которая показывает обновление аналитики.

Дальше читаем сообщения.

message GetUserProgressAnalyticsRequest {
  string user_id = 1;
  AnalyticsPeriod period = 2;
}

Это структура запроса для получения аналитики пользователя. Поле user_id имеет тип string, но это не значит, что допустима любая строка. С точки зрения тестирования всё равно остаются вопросы: что будет с пустым user_id, несуществующим пользователем, некорректным форматом идентификатора или пользователем без курсов.

Поле period имеет тип AnalyticsPeriod. Это enum, то есть ограниченный набор допустимых значений. Значит, нужно отдельно посмотреть, какие периоды поддерживаются.

Ответ описан так:

message UserProgressAnalyticsResponse {
  string user_id = 1;
  int32 total_courses = 2;
  int32 completed_courses = 3;
  double completion_rate_percent = 4;
  AnalyticsPeriod period = 5;
  UserProgressSegment segment = 6;
  google.protobuf.Timestamp calculated_at = 7;
}

Из ответа видно, какие данные возвращает сервис:
- общее количество курсов пользователя,
- количество завершённых курсов,
- процент завершения,
- период расчёта,
- сегмент пользователя
- и время расчёта аналитики

Поле segment показывает, в какой сегмент попал пользователь. Например, NO_PROGRESS, LOW, MEDIUM или HIGH. Но сам контракт не объясняет, где проходят границы этих сегментов. Это хороший пример вопроса к требованиям: при каком проценте пользователь считается LOW, а при каком уже MEDIUM.

Второй ответ связан со сравнением пользователя с платформой:

message UserPlatformComparisonResponse {
  string user_id = 1;
  double user_completion_rate_percent = 2;
  double platform_average_completion_rate_percent = 3;
  double difference_from_average_percent = 4;
  int32 user_rank = 5;
  int32 total_users_in_rating = 6;
}

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

Также здесь есть рейтинг. Поля user_rank и total_users_in_rating требуют дополнительных бизнес-правил. Нужно понять, как считается место пользователя, что происходит при одинаковом проценте, участвуют ли пользователи без курсов и входят ли в рейтинг неактивные пользователи.

Следующий блок показывает список:

message ListTopUsersResponse {
  repeated TopUserItem items = 1;
  string next_page_token = 2;
}

Ключевое слово repeated означает коллекцию. В JSON это было бы похоже на массив объектов.

Поле next_page_token указывает на пагинацию. Это не номер страницы, а токен следующей страницы. Для тестирования это отдельная зона: первая страница, последняя страница, пустой результат, некорректный токен, слишком большой page_size.

Теперь посмотрим на запрос для потокового метода:

message WatchAnalyticsUpdatesRequest {
  AnalyticsPeriod period = 1;
  optional UserProgressSegment segment = 2;
}

Поле segment объявлено как optional. Технически это обычно означает возможность отсутствия поля в запросе. С точки зрения бизнес-логики в нашем случае это означает, что клиент может подписаться либо на общую аналитику, либо на обновления по конкретному сегменту пользователей.

Событие потока описано так:

message AnalyticsUpdatedEvent {
  AnalyticsPeriod period = 1;
  double platform_average_completion_rate_percent = 2;
  int32 total_users = 3;
  google.protobuf.Timestamp occurred_at = 4;
}

Это сообщение сервер может отправлять несколько раз в рамках одного соединения. Здесь важно не путать обычный request-response метод и streaming-метод. В потоковом сценарии нужно проверять не только структуру одного события, но и поведение соединения: приходят ли события после обновлений, что происходит при долгом ожидании, как работает фильтрация по периоду и сегменту.

Отдельно читаем enum-ы.

enum AnalyticsPeriod {
  ANALYTICS_PERIOD_UNSPECIFIED = 0;
  ANALYTICS_PERIOD_LAST_30_DAYS = 1;
  ANALYTICS_PERIOD_LAST_90_DAYS = 2;
  ANALYTICS_PERIOD_ALL_TIME = 3;
}

Enum показывает допустимые периоды аналитики. Значение с номером 0 обычно используется как значение по умолчанию. Но это не значит, что оно является корректным бизнес-сценарием. Часто UNSPECIFIED нужно проверять отдельно: сервис может отклонить такой запрос и потребовать явно передать период.

enum UserProgressSegment {
  USER_PROGRESS_SEGMENT_UNSPECIFIED = 0;
  USER_PROGRESS_SEGMENT_NO_PROGRESS = 1;
  USER_PROGRESS_SEGMENT_LOW = 2;
  USER_PROGRESS_SEGMENT_MEDIUM = 3;
  USER_PROGRESS_SEGMENT_HIGH = 4;
}

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

Если посмотреть на контракт как на учебный пример, в нём есть основные типовые конструкции, которые полезно уметь читать:

service показывает набор методов.

rpc показывает конкретные операции.

message описывает структуры запросов и ответов.

enum задаёт ограниченный набор значений.

repeated показывает список.

optional показывает поле, которое может отсутствовать.

stream показывает потоковую передачу событий.

import показывает использование внешнего типа.

Timestamp показывает работу с датой и временем.

gRPC-контракт нужно читать не как набор технических строк, а как описание поведения интеграции. Из него нужно извлекать вопросы.

  • Какие методы доступны?
  • Какие сообщения используются как запросы и ответы?
  • Какие поля обязательны по бизнес-смыслу?
  • Какие поля могут отсутствовать?
  • Где используются enum-ы?
  • Какие значения enum-а являются реальными бизнес-значениями, а какие похожи на техническое значение по умолчанию?
  • Где есть расчётные поля?
  • Какие граничные случаи следуют из этих расчётов?
  • Где используется список?
  • Есть ли пагинация?
  • Есть ли потоковая передача данных?

И главное: контракт не заменяет требования. Он показывает форму интеграции, но не всегда раскрывает бизнес-правила. Например, из схемы видно, что есть процент завершения курсов, но не видно, как округлять значение, учитывать ли архивные курсы, что делать с пользователем без курсов и как определять границы сегментов.

Поэтому для QA gRPC-контракт - это точка входа в анализ. Он помогает понять структуру API, найти зоны риска и сформулировать вопросы к требованиям.

В конце предлагаю потренироваться на небольшом интерактивном примере:

https://aklimenkoschool.ru/simulators/grpc-read/

открыть страницу с .proto-контрактом, прочитать его сверху вниз и пройти квиз по схеме. Задача упражнения не в том, чтобы запомнить весь синтаксис Protocol Buffers, а в том, чтобы научиться быстро видеть в контракте методы, сообщения, типы данных, enum-ы и возможные тестовые сценарии.

Report Page