Разбираемся в автотестах GRPC без боли и седых волос
Всем привет! Меня зовут Катя Муличева, я тестировщица в СИБУР Диджитал. Последние два года я пишу автотесты для gRPC на Kotlin. Получив опыт, я захотела написать статью, которая очень пригодилась бы мне самой, когда я только начинала разбираться с gRPC. Надеюсь, вы найдете её полезной для себя!
Немного теории
Для чего нужен API
Начнём с основ. У нас есть сервисы, которые могут быть написаны на разных языках и использовать разные протоколы. Чтобы они могли между собой общаться, нам нужно организовать способ передачи сообщений. Таких способов существует множество: например, RPC или придуманный гуглом gRPC.
gRPC использует для общения формат protobuf, также придуманный Google. Хотя можно использовать и другие форматы — говорят, не протобафом единым живет gRPC. Но здесь я буду писать только про protobuf.
Когда может пригодиться gRPC
1) Если в проекте используются микросервисы, которые общаются друг с другом;
2) Если в проекте много разных частей написаны на разных языках программирования — у gRPC есть кодогенерация для многих популярных языков;
3) Если данные надо стримить — gRPC умеет с этим работать, потому что использует протокол HTTP/2, в котором легко работать с потоками;
4) Если важна скорость передачи — gRPC работает быстрее, потому что использует данные для передачи в бинарном виде. И ещё из-за других новшеств HTTP/2, но в них не будем углубляться.
Так как именно работает GRPC?
Делаем прото-запрос и получаем прото-ответ. Похоже на привычный рест? Да, только есть особенность: передаваемые и получаемые данные сохраняются в бинарном виде, а ключи для данных — это индексы, а не привычные в ресте имена полей. Так что данные передаются быстро, но выглядят непонятно для человека, будучи зашифрованными в бинарном виде.
Схема примерно такая
Поскольку этот текст написан для тестировщиков, мы будем рассматривать как работать с клиентом gRPC, а сервер писать не будем.
Чтобы работать с gRPC понадобится :
1) Прото-файл (который выступит для нас документацией какие прото-запросы и прото-ответы мы можем получать);
2) Стаб — это клиент для сервиса, описанного в прото-файле. Мы создадим его, используя кодогенерацию gRPC, или стабом выступит само приложение, через которое мы делаем запрос;
3) Адрес 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) {
runApplication(*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{
kotlinOptions.jvmTarget = "1.8"
}
tasks.withType{
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.
Эпилог
На сегодня всё! Спасибо, что прошли этот подробный гайд вместе с нами, и дайте знать, если хотите больше подобных материалов :)