Часть первая, GraphQL
Часть вторая, Websocket
Часть третья, (вы здесь)
Итак, мы с вами добрались до третьей, самой «хардовой» части цикла. Сегодня поговорим про gRPC.
Что такое gRPC?
Сам RPC — удалённый вызов процедур (иногда вызов удалённых процедур; RPC от англ. remote procedure call) — класс технологий, позволяющих программам вызывать функции или процедуры других программ, делая это так, как если бы они находились в одном адресном пространстве. Буква g в названии — это гугловая реализация этих технологий.
Разберем это все на примере.
Допустим, что вы — программист и сидите в монолитной репе. У вас одно приложение. Сам проект открыт в IDE и вы в нем работаете. В репе реализован определенный класс (например, на Kotlin), у которого есть метод, возвращающий вам данные по пользователю.
fun getUserInfo(id: String) {
return //some data
}
Теперь в другом месте проекта, в другом классе, вы хотите вызвать этот метод, чтобы поработать с данными о пользователе. Что вы тогда делаете? Импортируете класс и просто делаете обычный вызов метода.
var userData = getUserInfo("userID")
// Continue work with data
Он выполнится, вы получите все нужные данные. Всё здорово и всё работает.
Дальше приходит новый вызов - микросервисная архитектура, которая предлагает всё разделить и вынести всё, что мы делаем с пользователями, в отдельный сервис.
Теперь наши условные сервисы разделило сетевое взаимодействие, вот как на картинке.
Есть сервис A, который хочет данные от пользователя. Есть сервис B, у которого есть публичный API, через который эти данные можно получить.
Что делать сервису A?
Ему нужно сделать get-запрос на endpoint, который предоставит сервис B, затем получить обратно HTTP-ответ (там будет JSON), извлечь данные и работать с ними.
Что предлагает gRPC?
А предлагает он вот что — пусть сервис B заведет некий протофайл с расширением .proto, в котором опишет те методы и функции, которые можно будет вызывать у него всем остальным.
Получается такой аналог API, в котором сервис подтверждает, что может работать с вызовами этих функций с определенными параметрами и возвращать определенные данные. При этом внутри себя он на самом деле реализует все эти функции.
И на своем уровне поднимает RPC-службу.
Эта служба обрабатывает все запросы, которые будут приходить.
Окей, сервисом В разобрались. Что теперь делать на уровне сервиса А, чтобы получить данные о пользователе?
Мы определенным образом забираем себе модели, описанные в .proto file сервиса В. Например, подключаем в gradle через dependency этот пакет, в котором есть все описанные модели.
И дальше реализуем над ними обёртку со своей логикой и вызовом методов сервиса В.
Теперь мы начинаем писать код, опираясь именно на вызовы нашей обёртки, которая лежит у нас здесь в проекте, это уже удобней. При этом, когда мы будем вызывать свои методы, они внутри себя на самом деле будут обращаться к методам сервиса B. Они пойдут к нему в RPC-службу и скажут, что хотят с ним работать. Сервис B ответит, что готов с удовольствием нам помочь.
При этом стоит заметить, что в gRPC все запросы используют Protocol Buffers — это специальный бинарный протокол сериализации данных, разработанный в компании Google. То есть сервис А предоставит сервису В свой запрос в бинарном виде. Сервис В поймёт, какой метод у него вызывают, выполнит его и вернет результат назад. Причем ответ будет также проходить через RPC службу и будет в двоичном виде. Сервис А уже получит такой ответ и продолжит работать дальше, как ни в чем не бывало.
Если чуть тщательнее порыться под капотом, получится такая схема.
На клиентской машине мы вызываем метод. Например, нам нужна информация о пользователях, и вызываем его так, как будто находимся на самом деле в сервисе B.
При этом мы делаем обращение именно в реализованную ранее обёртку.
Затем уже подрубаются специальные библиотеки, которые наш запрос транслируют определенным образом в двоичный запрос. Проводят сериализацию, передают его уже дальше в RPC-службу, и далее через protocol buffers всё попадает на сервер B.
Он в свою очередь обратно десериализует эту двоичную последовательность в виде имени процедуры, параметров и значения. Выполняет ее, получает данные, а затем обратно проделывает всю эту трансформацию.
Данные запаковывает в бинарную последовательность и отправляет ее обратно по сети.
Мы на стороне клиента ее получаем, распаковываем данные, возвращаем обратно нашей программе и продолжаем работать дальше.
Такой алгоритм, если в двух словах.
Честно скажу, когда я изучал эту технологию, первое, о чем я подумал — что за жесть вообще? Зачем так усложнять всё, можно же проще все делать. Есть же REST HTTP, он понятен, там есть endpoint-ы, нормальные запросы и ответы, которые можно увидеть и прочитать, увидеть данные которые передаем и получаем, их можно осмыслить. Зачем вообще все эти бинарные вещи?
На самом деле, тут есть три очень важные штуки.
Во-первых, и это по сути главная киллер-фича — скорость. Сам Google заявлял, что прирост по скорости будет в 3—10 раз, энтузиасты же, которые все это протестировали, вывели свою цифру — в 7 раз. В 7 раз быстрее происходит сериализация данных при работе с этим бинарным форматом, чем при работе с JSON.
Поэтому, если ваши сервисы, что называется, gRPC-ориентированы, то есть заточены на очень быстрые взаимодействия друг с другом, время отклика максимально быстрое, при этом очень маленькие короткие сообщения — вы получаете колоссальный прирост по производительности и эффективности потребления ресурсов. Это очень важный кейс.
Во-вторых, помните ситуацию при работе с REST HTTP, когда ребята сделали endpoint, а документацию забыли? А тестировать-то как-то надо. Поэтому в этой случае документацией станет сам разработчик.
В gRPC подобное не прокатит — здесь вы сначала реализовываете именно контракт в протофайле. То есть вы как бы реализуете сначала документацию, а потом — логику. На мой взгляд, это тоже очень важная фишка
В-третьих, здесь очень сильная обратная совместимость за счет нежесткой привязки к имени поля. Двоичный протокол не знает, что такое строки, он знает, что такое числа. Определенный способ записи и организации данных в протофайле исключает ситуацию, когда разработчик поменял имя переменной, и у клиента что-то отлетело.
На практике
Давайте перейдем к практике. Для начала я выкачиваем проект отсюда. Собираем и запускаем его согласно документации
Далее заходим с вами в Postman -> NEW и выбираем gRPC.
Прежде чем начнем работать, надо объяснить Postman, с каким сервисом мы будем иметь дело. Как это делается? Правильно! Надо подгрузить в него протофайл сервиса.
Переходим во вкладку Service definition.
Кнопка “Import .proto file” так и просится быть нажатой.
Нажимаем ее и далее выбираем файл.
Нужный нам протофайл будет лежать в папке со скачанным проектом, подпапка proto. Имя GrpcExampleService.proto
Выбираем его и жмем “Next”.
Postman увидел наш файл и понял, что мы будем работать с некоторым сервисом, поэтому предложит импортировать его. Я не против, поэтому жмем “Import as API”.
После успешного импорта мы увидим, что наш созданный пустой gRPC Request использует наш новый API.
Что после этого изменилось?
Вы можете нажать на селектор выбора метода.
Ого! Тут нас уже ждут все методы, с которыми умеет работать сервис.
Прикольно, а можно как-то проверить, что тут реально все методы?
Можно, заходим в левом меню Postman в раздел APIs, далее выбираем наш New API — Definition и кликаем на протофайл.
Для любознательных — можете изучить его весь, а я покажу только первые 10 строк, где у нас лежит информация о том, какие методы “открыты наружу”. Смотрим блок service.
Действительно, 3 метода — Postman не обманул, но не протестировать его я не мог ?.
Хорошо, теперь возвращаемся к нашему запросу.
Давайте выполним первый запрос на добавление нового клиента.
Для этого сперва введем в поле url “localhost:50051”, так как сервис поднят на этом порту.
А в селекторе метода выберем “AddClient” и перейдем во вкладку “Message”.
Message у нас в виде JSON-объекта, добавляем фигурные скобки и внутри вводим первый символ “c” C, и Postman сразу предлагает напечатать clientsinfo, подсказки тут как тут — спасибо тебе.
Можем подсмотреть в протофайле, что там в clientinfo надо передать — login, email, city.
Окей, давайте сформируем такой объект.
{
"clientsinfo": [
{
"city": "City",
"email": "check",
"login": "Habra"
}
]
}
Выполним запрос, увидим Response: OK.
Попробуем получить информацию по этому логину. Для этого меняем вызываемый метод в селекторе на GetClientByLogin:
А в теле сообщения указываем:
{
"login": "Habra"
}
Выполняем запрос — и получаем всю введенную нами ранее информацию.
Вот и вся магия, несложно, да?
Коды ответов
Стоит обратить внимание, что у gRPC есть свои коды ответов. У Google всё круто с документацией, поэтому прямо в репозитории gRPC есть вся информация о кодах ответа и их значениях,
Что нужно тестировать в gRPC?
Сразу хочу сделать маленькое отступление и сказать вот о чем. Так как это удаленный вызов процедур, то проверка логики работы самих процедур отлично покрывается юнит-тестами и нет необходимости тестировать их работу через внешние вызовы. К сожалению, в практике встречаются разные проекты, и могут быть те, где проверки юнит-тестами заменяют на внешние вызовы процедур для проверки их логики работы. В таком случае к списку ниже придется добавить проверку логики работы самих вызываемых процедур.
Окей, если логика работы покрыта юнит-тестами, то на что еще мне как тестировщику стоит обратить внимание?
Прежде всего, не забываем, что gRPC – это удаленный вызов процедур. Поэтому мы с вами должны проверить, что наша процедура с определенными параметрами может быть вызвана удаленно. Потому что, возможно, она уже и не работает через удаленный вызов.
Далее надо проверить, доступна ли процедура извне. Закрытие авторизацией никто не отменял
Круто, если будет проверен формат данных, который возвращается. В примерах выше возвращался JSON. Его, например, можно провалидировать по схеме
Ну и коды ответов. Это уже на ваше усмотрение, согласно бизнес-потребности
Подведем итог, что надо для тестирования gRPC-сервиса
Импортим .proto файл
Проверяем доступность удаленного вызова процедур
Проверяем доступность методов закрытых авторизацией
Проверяем возвращаемые данные и ошибки (при необходимости, т.к. может быть покрыто юнит-тестами)
Проверяем е2е-сценарии
Проверяем коды ответов
На этом я буду заканчивать свой цикл из трех статей по трем совершенно разных не-REST-бэкендам.
Несмотря на то, что цикл занял три поста, мы пробежались лишь по верхушкам этих трех айсбергов. Под каждой технологией ещё очень и очень много особенностей. Но, надеюсь, мои посты пригодятся вам и помогут легче подступиться к этим вещами, чтобы что-то протестировать, попробовать, пощупать, да и просто познакомиться. А потом уже, набравшись опыта, можно начать наращивать качество своего тестирования.
Всем успехов и добра!