Всем привет! Меня зовут Катя Муличева, я тестировщица в СИБУР Диджитал. Последние два года я пишу автотесты для gRPC на Kotlin. Получив опыт, я захотела написать статью, которая очень пригодилась бы мне самой, когда я только начинала разбираться с gRPC. Надеюсь, вы найдете её полезной для себя!
Немного теории
Для чего нужен API
Начнём с основ. У нас есть сервисы, которые могут быть написаны на разных языках и использовать разные протоколы. Чтобы они могли между собой общаться, нам нужно организовать способ передачи сообщений. Один из таких способов — RPC и его модификация от Google — gRPC.
gRPC использует для общения формат protobuf, также придуманный Google. Хотя можно использовать и другие форматы — говорят, не протобафом единым живет gRPC. Но здесь я буду писать только про protobuf.
Когда может пригодиться gRPC
Если в проекте используются микросервисы, которые общаются друг с другом;
Если в проекте много разных частей написаны на разных языках программирования — у gRPC есть кодогенерация для многих популярных языков;
Если данные надо стримить — gRPC умеет с этим работать, потому что использует протокол HTTP/2, в котором легко работать с потоками;
Если важна скорость передачи — gRPC работает быстрее, потому что использует данные для передачи в бинарном виде. И ещё из-за других новшеств HTTP/2, но в них не будем углубляться.
Так как именно работает GRPC?
Делаем прото-запрос и получаем прото-ответ. Похоже на привычный рест? Да, только есть особенность: передаваемые и получаемые данные сохраняются в бинарном виде, а ключи для данных — это индексы, а не привычные в ресте имена полей. Так что данные передаются быстро, но выглядят непонятно для человека, будучи зашифрованными в бинарном виде.

Поскольку этот текст написан для тестировщиков, мы будем рассматривать как работать с клиентом gRPC, а сервер писать не будем.
Чтобы работать с gRPC понадобится :
Прото-файл (который выступит для нас документацией какие прото-запросы и прото-ответы мы можем получать);
Стаб — это клиент для сервиса, описанного в прото-файле. Мы создадим его, используя кодогенерацию gRPC, или стабом выступит само приложение, через которое мы делаем запрос;
Адрес API, куда будем слать запросы.
Что можно использовать для тестирования gRPC?
Традиционно предлагают использовать BloomRpc для Windows. Все просто: добавляешь прото-файл, вводишь адрес API. В нужном запросе дополняешь поля данными и запускаешь. Клиент — само приложение BloomRpc.
Для MACos есть другой инструмент, такой же простой: grpc_clicker. Вот ссылка на гитхаб и на маркетплейс VSCode.
Ещё есть мультиплатформенный Postman, в котором теперь тоже поддерживается возможность использовать gRPC.
Если вы подгрузили файл и выполняете запрос, но все равно ничего не работает — возможно, дело в том, что у вас перед выполнением запроса требуется авторизация еще каким-нибудь API. Самый простой вариант решения: отключите авторизацию и попробуйте еще раз. Ввод пароля и логина, получение токена перед выполнением запроса можно попробовать настроить в Postman, но это трудоёмкий процесс.
И к практике
Переходим к созданию автотестов. С нуля и пошагово!
Мы будем писать автотесты по прото-файлу Dogs вот отсюда, на языке Kotlin. А для сборки будем использовать Spring Boot и Gradle.
Предварительная настройка
Открываем InteliIjIDE. Выбираем Console Application и 11 версию Java. В качестве сборщика будем использовать Gradle.


Ура, заготовка есть. Теперь ждем некоторое время, пока проект собирается (внизу справа Gradle Build с полосой загрузки):

После того как все собралось — делаем Package.

И перетаскиваем в него Main.kt. Это нужно для корректного запуска.

Теперь поменяем код в нашем файле main. Оригинал выглядит примерно так:

А меняем мы его вот на это:
package t import org.springframework.beans.factory.annotation.Value import org.springframework.boot.CommandLineRunner import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication class DogsTestApp() : CommandLineRunner { companion object { @JvmStatic fun main(args: Array<String>) { runApplication<DogsTestApp>(*args) } } override fun run(vararg args: String?) { } }
Мы будем запускать приложение, используя SpringBoot. Дальше мы дополним этот файл, но пока оставим такую заготовку.
На этом этапе сделаем папку proto, где будем хранить наши прото-файлы.


Помещаем туда наш прото-файл:

И вот так он выглядит:

Теперь идём в файл build.gradle.kts:

Сейчас в нем автоматически сгенерированный код, который мы заменим кодом ниже. В этом файле мы прописываем, какие именно версии библиотек нам нужны. Обратите внимание, что здесь надо указать название Package, который мы создали в начале, в этой строке “group= "t"”.
И еще здесь мы указываем класс, который будет использоваться для запуска приложения
springBoot{ mainClass.set("t.DogsTestApp")
Код, который заменит автоматический:
import com.google.protobuf.gradle.id import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins{ id("org.springframework.boot")version"2.7.0" id("io.spring.dependency-management")version"1.1.0" id("com.google.protobuf")version"0.9.2" kotlin("jvm")version"1.7.22" kotlin("plugin.spring")version"1.7.22" } group= "t" version= "0.0.1-SNAPSHOT" java.sourceCompatibility= JavaVersion.VERSION_11 ext["grpcKotlin"] = "1.3.0" ext["grpcVersion"] = "1.53.0" ext["protobufVersion"] = "3.22.0" repositories{ mavenCentral() } springBoot{ mainClass.set("t.DogsTestApp") } protobuf{ protoc{ artifact = "com.google.protobuf:protoc:${rootProject.ext["protobufVersion"]}" } plugins{ id("grpc"){ artifact = "io.grpc:protoc-gen-grpc-java:${rootProject.ext["grpcVersion"]}" } id("grpckt"){ artifact = "io.grpc:protoc-gen-grpc-kotlin:${rootProject.ext["grpcKotlin"]}:jdk8@jar" } } generateProtoTasks{ all().forEach{ it.plugins{ id("grpc") id("grpckt") } it.builtins{ id("kotlin") } } } } dependencies{ implementation("org.springframework.boot:spring-boot-starter") implementation("net.devh:grpc-client-spring-boot-starter:2.14.0.RELEASE") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("io.grpc:grpc-kotlin-stub:${rootProject.ext["grpcKotlin"]}") implementation("io.grpc:grpc-protobuf:${rootProject.ext["grpcVersion"]}") implementation("com.google.protobuf:protobuf-kotlin:${rootProject.ext["protobufVersion"]}") implementation("com.google.protobuf:protobuf-java:${rootProject.ext["protobufVersion"]}") runtimeOnly("io.grpc:grpc-netty-shaded:${rootProject.ext["grpcVersion"]}") compileOnly("org.apache.tomcat:annotations-api:6.0.53") testImplementation("org.springframework.boot:spring-boot-starter-test") } tasks.withType<KotlinCompile>{ kotlinOptions.jvmTarget = "1.8" } tasks.withType<Test>{ useJUnitPlatform() }
Если вы сделали всё описанное выше, то на этом этапе у вас должны появиться вот такие значки. Они показывают, что надо подгрузить файлы и собрать проект заново.

Нажимаем сперва на один значок файла, затем на другой (на слона). Мы изменили файл настроек, и теперь эти изменения должны вступить в силу с загрузкой нужных нам библиотек.
После того как все подгрузится, красный текст (на скрине) пропадет.

Далее идем в settings.gradle.kts.

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

Создаём клиента
Создаём new package service, а в нём — new Kotlin class под названием DogsClientService. В учебных целях будем рассматривать симулятор собаки. Да.


Также не забудем на этом шаге сбилдить прото-файлы, чтобы нам удобнее было писать автотест, используя их. Для этого нужно сперва открыть Gradle сбоку и сделать двойной клик на clean. А затем сделать двойной клик на build.
Теперь возвращаемся к DogsClientService, делаем импорты и нотации

И начинаем описывать наш стаб. Поскольку мы сбилдили с помощью Gradle наши прото- файлы (это можно увидеть, если открыть сбоку extracted-protos ), мы увидим появление нашего DoggRPC.

И значит теперь, благодаря кодогенерации, у нас будут появляться подсказки!

Выбираем BlockingStub, потому что у нас обычный запрос, не потоковый и не асинхронный. Ниже мы описали, что этот клиент будет работать с запросом для гавканья:

А запрос и ответ известны нам из прото-файла.

Фактически, мы написали, что наш клиент будет работать с этим запросом.
Теперь сделаем класс DogSteps, в котором напишем метод, отправляющий наш запрос для гавканья makeBark.
Сейчас у нас очень простой вызов метода API с запросом без параметров, поэтому строчки 11 и 13 кажутся избыточными, но в дальнейших примерах мы увидим, что здесь можем добавлять данные в наш request .
А пока делаем его таким и на 14 строке пишем вызов метода API barkRequest, который написали в клиенте DogsClientService (обратите внимание, что в 7 строке мы создали экземпляр этого клиента) с нашим запросом request2. И кладем его в переменную getResponse — данные, которые нам придут после выполнения запроса.

Идем в Main.kt и делаем импорт командойimport t.DogSteps.
Затем дописываем в DogsTestApp переменную внутри скобок (private val mydogSteps:DogSteps) и переписываем метод run, в котором мы через созданную переменную mydogStep запускаем вызов метода MakeBark.
Остаётся заключить наш вызов метода в try catch, чтобы можно было увидеть ошибки, если запрос не сможет выполниться.

И последний шаг — в resources создаем файл application.properties, в котором пишем адрес, где находится API, к которому мы будем обращаться.

И теперь мы можем запускать наш API-тест, нажав на зеленую стрелку!
Правда сейчас он у нас не выполнится, потому что адрес static://your_name:1234 не содержит в себе ручку нашего API. Чтобы увидеть запуск придется поднимать у себя локально API с нашим прото-файлом или, используя этот пример как основу, написать запрос к своему API, который вы тестируете.
Полезные советы.
3.1. Для сложных мессаджей в прото-файле.
Возьмём исходный message BarkRequest {}и допишем в него уточняющие параметры — кто гавкает, где, почему и сколько раз (и не забудьте пересобрать через Gradle clean и затем build).
message BarkRequest { string whoBark = 1; string whereBark = 2; string whyBark = 3; int howManyTimesBark = 4; }

Идём в DogSteps, и теперь наш запрос будет не такой простой, как раньше — мы увидим предложенные варианты методов заполнения созданных полей.

Допишем в параметры метода, что будем ожидать эти данные для заполнения полей и в request1. А дальше все как прежде.

И соответственно указываем данные при вызове:

3.2. Assertion.
Без Assertion автотесты не обходятся, и в нашей конфигурации мы можем использовать, например, такую библиотеку:
import org.hamcrest.MatcherAssert import org.hamcrest.core.Is
И пример её использования:
val sendingPeriod = myConstantForCheck val a = somePage.sendingPeriod.text var b = a.replaceAfter(" ", "") println(b.toString()) println("ffffff") MatcherAssert.assertThat( "is equal", b, Is.`is`(sendingPeriod + " ") )
3.3. log info
Вывод данных в консоль тоже очень полезен. Подключаем:
import org.slf4j.LoggerFactory private val log = LoggerFactory.getLogger(*javaClass*)
И пара примеров использования:
# Первый log.info(request2.*whoBark*.toString()) # Второй log.info(getResponse.*message*.toString())
3.4. А если наше сообщение использует другое сообщение?
Допустим, наш прото-файл бы выглядел еще сложнее. С новым мессаджем PlayRequest.

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

А в DogsClientService допишем метод для нового запроса playRequest.

И теперь в DogSteps вот так заполним наш запрос, используя созданный класс toyFactory.

Эпилог
На сегодня всё! Спасибо, что прошли этот подробный гайд вместе с нами, и дайте знать, если хотите больше подобных материалов :)

