Pull to refresh

Разбираемся в автотестах gRPC без боли и седых волос

Level of difficultyMedium
Reading time7 min
Views5.6K

Всем привет! Меня зовут Катя Муличева, я тестировщица в СИБУР Диджитал. Последние два года я пишу автотесты для gRPC на Kotlin. Получив опыт, я захотела написать статью, которая очень пригодилась бы мне самой, когда я только начинала разбираться с gRPC. Надеюсь, вы найдете её полезной для себя!

Немного теории

Для чего нужен API

Начнём с основ. У нас есть сервисы, которые могут быть написаны на разных языках и использовать разные протоколы. Чтобы они могли между собой общаться, нам нужно организовать способ передачи сообщений. Один из таких способов — RPC и его модификация от Google — 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.

Ура, заготовка есть. Теперь ждем некоторое время, пока проект собирается (внизу справа 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.

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

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

    Создаём 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, который вы тестируете.

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

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.

Эпилог

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

Tags:
Hubs:
Total votes 9: ↑7 and ↓2+5
Comments1

Articles

Information

Website
sibur.digital
Registered
Employees
1,001–5,000 employees
Location
Россия