Разбираемся в автотестах 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.

  1. Предварительная настройка

Открываем InteliIjIDE. Выбираем Console Application и 11 версию Java. В качестве сборщика будем использовать Gradle.

5ec28d7b507c0aa0e2bd2f120ca063db.png1f0a172a4c4f0560a9d3e4a368b4e04b.png

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

1364442fb6f9bcfcbc7122a67bffbd70.png

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

8ecc2f1f4104328dba943453bc0cd452.png

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

dea3244d7cf8089e4bb248f94e522f6d.png

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

83e55cee1b5acb15db534f9c563abdaa.png

А меняем мы его вот на это:

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, где будем хранить наши прото-файлы.

ebcd0cbe9a812cc7c97a36c51773beb6.pngeb9cba537c7fa6925c0f6e510e390c0b.png

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

d2de509feb41026a60ef3cacdcd42993.png

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

4ede820ea6d09309e4c872806dbbaa9f.png

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

a3d876d4d00c87b495295e6bf42327ad.png

Сейчас в нем автоматически сгенерированный код, который мы заменим кодом ниже. В этом файле мы прописываем, какие именно версии библиотек нам нужны. Обратите внимание, что здесь надо указать название 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()
}

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

aa8caa7a6c5aa14b0f9c200f65fea3e9.png

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

После того как все подгрузится, красный текст (на скрине) пропадет.

a565425c6099b37f4db0c2c44ab5a250.png

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

53989a68f559e55023209cdaa8f7235f.png

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

e310b542fa484c517099e11b7d9d0cf7.png

  1. Создаём клиента

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

b1330552ef229103f3d0471dd7c3b6ab.png4bca3ac30c85165fb5508c8c7d12aeb4.png

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

Теперь возвращаемся к DogsClientService, делаем импорты и нотации

e9a851d2f9aff2084cdae0f961c80b99.png

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

4f8df0b6e3de3497177da6634e93b303.png

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

cb60032d1ae6869664ddffadc740f3bd.png

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

be92bc0a096c6491c09cf1e31b4b3ef6.png

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

cbb9d71d6323b5bab70bf3b60c6d306a.png

Фактически, мы написали, что наш клиент будет работать с этим запросом.

Теперь сделаем класс DogSteps, в котором напишем метод, отправляющий наш запрос для гавканья makeBark.

Сейчас у нас очень простой вызов метода API с запросом без параметров, поэтому строчки 11 и 13 кажутся избыточными, но в дальнейших примерах мы увидим, что здесь можем добавлять данные в наш request .

А пока делаем его таким и на 14 строке пишем вызов метода API barkRequest, который написали в клиенте DogsClientService (обратите внимание, что в 7 строке мы создали экземпляр этого клиента) с нашим запросом request2. И кладем его в переменную getResponse — данные, которые нам придут после выполнения запроса.

49392d8a6dee0f10da22dc91bbbf8886.png

Идем в Main.kt и делаем импорт командойimport t.DogSteps.

Затем дописываем в DogsTestApp переменную внутри скобок (private val mydogSteps: DogSteps) и переписываем метод run, в котором мы через созданную переменную mydogStep запускаем вызов метода MakeBark.

Остаётся заключить наш вызов метода в try catch, чтобы можно было увидеть ошибки, если запрос не сможет выполниться.

5937211ce95f4156db74d2b43111b57c.png

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

d2b1b4c135622f86ed61881c3222052d.png

И теперь мы можем запускать наш API-тест, нажав на зеленую стрелку!

Правда сейчас он у нас не выполнится, потому что адрес static://your_name:1234 не содержит в себе ручку нашего API. Чтобы увидеть запуск придется поднимать у себя локально API с нашим прото-файлом или, используя этот пример как основу, написать запрос к своему API, который вы тестируете.

  1. Полезные советы.

3.1. Для сложных мессаджей в прото-файле.

Возьмём исходный message BarkRequest {}и допишем в него уточняющие параметры —  ккто гавкает, где, почему и сколько раз (и не забудьте пересобрать через Gradle clean и затем build).

message BarkRequest {

string whoBark = 1;

string whereBark = 2;

string whyBark = 3;

int howManyTimesBark = 4;

}

4fdc206537a94af72960478161caa55b.png

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

892a6044be97273602d853d38e7350d2.png

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

842e405cb41c25f54fd75c94fe6ba9f1.png

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

1a8cefe98b554f102d4db00b8d4ba44f.png

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.

d1809832bb478aa79406f16b2ed78d0e.png

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

22bd88dd90552a77952532b6e8d56fa6.png

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

60c099264e7f4a23de9f6eb46ff28ef8.png

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

e44f4baab1109fa0876f849a7e4a52df.png

Эпилог

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

© Habrahabr.ru