Go, Allure и HTTP, или Как мило тестировать HTTP-сервисы на Go

от автора

Привет! Меня зовут Сергей, я старший разработчик в Ozon и раньше вообще не был замечен в QA.

Все мы привыкли к лёгкому написанию тестов на Python и Java — это основные языки автотестировщиков с богатым инструментарием утилит и всего, что упрощает жизнь. Что нужно для написания автотестов для HTTP-сервиса на Python или Java? Гугл, бутылочка крафта и два часа времени. 

А как быть в случае с Go? Как раз на нём мы в большинстве случаев пишем микросервисы. И если тесты написаны на другом языке, разработчики не могут внести в них свой вклад или отревьюить их. Поэтому внутри Ozon активно развивается Go-сообщество QA, и этим ребятам тоже нужно тестировать HTTP-сервисы и проверять отчёты в Allure. Как настоящие сварщики мы подумали: «Если чего-то не хватает, нужно написать своё». Сказано — сделано: встречайте опенсорс-библиотеку CUTE в BDD-стиле, которая облегчает тяготы создания автотестов и упрощает переход на Go. Главные фичи: создание HTTP-тестов, возможность реализовывать проверки из коробки, Allure-отчёты и низкий порог входа.

Дисклеймер: Мои коллеги ранее уже рассказывали, как у нас тестируют на Go. Также не так давно мы писали про опенсорс-библиотеку Allure-Go. А сегодня речь пойдёт о библиотеке CUTE (Create Your Tests Easily).

Моя команда делает бэкенд для мобильного приложения, но к нам не ходят по gRPC, как это принято в Ozon. Подходящих инструментов для тестирования HTTP-сервисов внутри компании раньше не было, а те, что существовали вне, не подходили нам и не работали с Allure (что было важно).

Мы решили облегчить тяготы наших тестировщиков и создать инструмент для тестирования HTTP-сервисов, который в итоге перерос в библиотеку.

Как было раньше?

Обычно тест состоит из следующих шагов:

  1. Подготовить HTTP-клиент.

  2. Создать данные для теста.

  3. Выполнить HTTP-запрос.

  4. Убедиться, что запрос выполнился.

  5. Считать ответ в структуру.

  6. Начать проверять структуру.

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

Ранее на Хабре уже рассказывали, как писать тесты для HTTP-сервисов в связке с Allure (рекомендую к прочтению, чтобы сравнить сложность подходов).

Как будет?

Рассмотрим самый простой кейс, когда нужно сделать запрос и проверить пару полей:

import ( "context" "net/http" "testing" "time"  "github.com/ozontech/cute" "github.com/ozontech/cute/asserts/json" )  func TestExample(t *testing.T) { cute.NewTestBuilder(). Title("Title").             // Задаём название для теста Description("Description"). // Придумываем описание // Тут можно ещё добавить много разных тегов и лейблов, которые поддерживаются Allure Create(). RequestBuilder( // Создаём HTTP-запрос cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodGet), ). ExpectExecuteTimeout(10*time.Second). // Указываем, что запрос должен выполниться за 10 секунд  ExpectStatus(http.StatusOK).          // Ожидаем, что ответ будет 200 (OK) AssertBody(                           // Задаём проверку JSON в response body по определённым полям json.Equal("$[0].email", "super@puper.biz"), json.Present("$[1].name"), ). ExecuteTest(context.Background(), t) } 

А если в тесте возникнут какие-то проблемы, то отчёт будет такой:

При этом мы поддержали все возможные теги и лейблы Allure.

Сейчас мы рассмотрели самый простой тест с минимальным количеством информации, проверок и без каких-либо дополнений.

Заинтересовались? Тогда давайте рассмотрим все возможности библиотеки.

Строим тест

Шаг 0. Начало начал

Всё с чего-то начинается. Наш тест начинается с подготовки сервиса, который в дальнейшем будет нам помогать создавать тесты cute.NewHTTPTestMakerSuite(opts ...Option)

testMaker := cute.NewHTTPTestMakerSuite(opts ...Option)

При инициализации testMaker вы можете указать базовые настройки для тестов, например HTTP-клиент, если это вам важно. А можете ничего не настраивать — и всё будет работать из коробки.

В дальнейшем testMaker будет нам пригождаться на протяжении всего пути создания тестов. Поэтому если вы создаёте пакет/набор тестов, то советую сохранить testMaker в общую для тестов структуру. Реализацию таких тестов можете найти в репозитории проекта.

Далее необходимо инициализировать сам билдер, его уже нужно создавать для каждого теста, так как он содержит в себе всю информацию о тесте.

testBuilder := testMaker.NewTestBuilder()

Если вам не важны настройки, то вы можете использовать заготовленный билдер:

cute.NewTestBuilder()

Отлично, на этом шаг 0 закончен! И мы готовы приступать к созданию теста.

Шаг 1. Информируем всех

Как это обычно бывает, нам необходимо указывать подробную информацию о тесте, чтобы в будущем не забыть его назначение. Благо Allure позволяет это сделать, и мы можем добавить много информации о тесте:

cute.NewTestBuilder(). Title("TestExample_Simple"). // Заголовок Tags("simple", "some_local_tag" ,"some_global_tag", "json"). // Теги для поиска Feature("some_feature"). // Фича Epic("some_epic"). // Эпик Description("some_description"). // Описание теста

В примере выше указаны не все лейблы, со всеми остальными вы можете познакомиться внутри проекта.

Теперь информация о тесте появится у нас в отчётах.

Шаг 2. Помни о прошлом, не забывай о будущем

Порой бывают ситуации, когда необходимо предварительно подготовить запрос или выполнить какие-то действия после прохождения теста.

Вы можете это сделать вне теста, но также предусмотрены методы для упрощения работы:

BeforeExecute(func(req *http.Request) error) AfterExecute(func(resp *http.Response, errs []error) error)

Данные методы не обязательны, поэтому вы можете их пропустить.

Пример:

cute.NewTestBuilder(). Create(). BeforeExecute(func(req *http.Request) error { /* Необходимо добавить реализацию */ return nil }). AfterExecute(func(resp *http.Response, errs []error) error { /* Необходимо добавить реализацию */ return nil })

Ещё есть AfterExecuteT и BeforeExecuteT, которые позволяют добавить информацию в Allure, например создать новый шаг:

func BeforeExample(t cute.T, req *http.Request) error { t.WithNewStep("insideBefore", func(stepCtx provider.StepCtx) { now := time.Now() stepCtx.Logf("Test. Start time %v", now) stepCtx.WithNewParameters("Test. Start time", now) time.Sleep(2 * time.Second) })        return nil }

Allure:

Шаг 3. Создаем запрос

Существует два способа передать запрос в тесте.

Если у вас уже создан *http.Request, вы можете просто воспользоваться Request(*http.Request):

req, _ := http.NewRequest(http.MethodGet, "http://google.com", nil)  cute.NewTestBuilder(). Create(). Request(req) // Передача запроса 

Второй способ подойдёт, если у вас нет подготовленного запроса. Необходимо вызвать RequestBuilder и самостоятельно собрать запрос.

Выглядит это примерно так:

cute.NewTestBuilder(). Create(). RequestBuilder(  // Создание запроса  cute.WithHeaders(map[string][]string{  "some_header":       []string{"something"}, "some_array_header": []string{"1", "2", "3", "some_thing"}, }), cute.WithURI("http://google.com"),  cute.WithMethod(http.MethodGet),    )

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

Отлично, самое скучное позади! Мы заполнили информацию о тесте, подготовили дополнительные шаги и создали запрос — настало время проверок!

Шаг 4. Доверяй, но проверяй!

Хочу заметить, что все проверки не обязательны и вы вольны выбирать, какие из них вам необходимы. Я же покажу все возможные варианты.

Response code

Начнём с элементарного — с проверки кода ответа. В этом нам поможет ExpectStatus(int).

Пример:

cute.NewTestBuilder(). Create(). Request(*http.Request). // Передача запроса  ExpectStatus(201) // Ожидаем, что ответ будет 201 (Created)

Allure:

JSON-схема

Перейдём к простому — к проверке JSON-схемы. Многим важно всегда иметь чёткую схему ответа.

Существует три способа проверить JSON-схему. Всё зависит от того, где она у вас находится.

  1. ExpectJSONSchemaString(string) — получает и сравнивает JSON-схему из строки.

  2. ExpectJSONSchemaByte([]byte) — получает и сравнивает JSON-схему из массива байтов.

  3. ExpectJSONSchemaFile(string) — получает и сравнивает JSON-схему из файла или удалённого ресурса.

Пример:

cute.NewTestBuilder(). Create(). Request(*http.Request). // Передача запроса  ExpectJSONSchemaFile("file://./project/resources/schema.json"). // Проверка response body по JSON schema

В случае ошибки отчёт в Allure будет таким:

Отлично, самые простые проверки позади. Пора проверить наши запросы по-настоящему!

Шаг 5. Я сказал, нам нужны настоящие проверки!

Настало время создать настоящие ассерты и проверить полностью наш response.

В библиотеке есть подготовленные ассерты для проверки заголовков, для работы с JSON в теле ответа, но также вы можете создавать свои.

Существует три типа проверок:

  1. Проверка тела ответа (response body)
    AssertBody и AssertBodyT

  2. Проверка заголовков ответа (response headers)
    AssertHeaders и AssertHeadersT 

  3. Полная проверка ответа (response)
    AssertResponse и AssertResponseT

Рассмотрим подробнее.

Проверка тела ответа (AssertBody)

Для проверок тела ответа в библиотеке есть несколько готовых решений.
Рассмотрим пример: допустим, нам надо из JSON достать поле “email” и убедиться, что значение равно “lol@arbidol.com”:

{   "email": "lol@arbidol.com" }

Для этого воспользуемся заготовленным ассертом из пакета:

cute.NewTestBuilder(). Create(). Request(*http.Request). // Передача запроса AssertBody(json.Equal("$.email", "lol@arbidol.com")) // Валидация поля “email” в response body

И это только один из примеров. В пакете есть целый набор ассертов:

  • Contains

  • Equal

  • NotEqual

  • Length

  • GreaterThan

  • LessThan

  • Present

  • NotPresent

В случае если ассерт не выполнился, в Allure появится красивый результат:

Проверка заголовков ответа (AssertHeaders)

С заголовками такая же история, как с телом ответа. Необходимо использовать:

AssertHeaders(func(headers http.Headers) error) AssertHeadersT(func(t cute.T, headers http.Headers) error)

Есть несколько готовых ассертов:

  • Present

  • NotPresent

Пример:

cute.NewTestBuilder(). Create(). Request(*http.Request). // Передача запроса    AssertHeaders(headers.Present("Content-Type")) // Проверка, что в заголовках есть “Content-Type” 

Полная проверка ответа (AssertResponse)

Когда необходимо проверить одновременно headers, body и ещё что-то из структуры http.Response, подойдёт:

   AssertResponse(func(resp *http.Response) error)    AssertResponseT(func(t cute.T, resp *http.Response) error)

Пример:

func CustomAssertResponse() cute.AssertResponse { return func(resp *http.Response) error { if resp.ContentLength == 0 { return errors.New("content length is zero") }   return nil } }

Шаг 6. Хочу быть самостоятельным!

В 5 шаге мы рассмотрели уже готовые ассерты и какие типы существуют. Но бывает, что хочется создать что-то своё и написать свой ассерт.

Для этого необходимо создать функцию, которая будет реализовывать один из типов:

  • type AssertBody func(body []byte) error

  • type AssertHeaders func(headers http.Header) error

  • type AssertResponse func(response *http.Response) error

  • type AssertBodyT func(t cute.T, body []byte) error

  • type AssertHeadersT func(t cute.T, headers http.Header) error

  • type AssertResponseT func(t cute.T, response *http.Response) error

Пример:

func customAssertHeaders() cute.AssertHeaders { return func(headers http.Header) error { if len(headers) == 0 { return errors.New("response without headers") }   return nil } }

Ещё есть функции c cute.T, которые позволяют добавить информацию в Allure, например создать новый шаг:

func customAssertBody() cute.AssertBodyT {    return func(t cute.T, body []byte) error {        step := allure.NewSimpleStep("Custom assert step")        defer func() {            t.Step(step)        }()          if len(body) == 0 {            step.Status = allure.Failed            step.WithAttachments(allure.NewAttachment("Error", allure.Text, []byte("response body is empty")))              return nil        }          return nil } 

И далее просто добавляете свой ассерт в тест:

cute.NewTestBuilder(). Create(). Request(*http.Request). // Передача запроса AssertHeaders(customAssertHeaders). AssertBodyT(customAssertBody)

Custom error

Вы могли заметить, что в результатах заготовленных ассертов существуют набор полей: Name, Error, Action и Expected.

Чтобы добавить такие данные в ваши кастомные ассерты, вы можете использовать:

cuteErrors.NewAssertError(name string, err string, actual interface{}, expected interface{}) error

Пример:

import (    cuteErrors "github.com/ozontech/cute/errors" )   func customErrorExample (t cute.T, headers http.Header) error {    return cuteErrors.NewAssertError("custom_assert", "example custom assert", "empty", "not empty")    // Пример создания красивой ошибки  } 

Соответственно, в Allure появится такая красота:

Optional asserts

Иногда необходимо добавить проверку, при невыполнении которой, тест не будет считаться проваленным.

import (    cuteErrors "github.com/ozontech/cute/errors" )  func customOptionalError(body []byte) error {     return cuteErrors.NewOptionalError(errors.New("some optional error from creator")) // Пример создания опциональной ошибки  } 

Шаг 7. Финал

Для создания самого теста нам нужно вызвать ExecuteTest(context.Context, testing.TB).

В ExecuteTest вы можете передать обычный testing.T или provider.T. Если вы используете allure-go, это не имеет значения — всё равно будет создан отчёт.

В итоге, если соединить все шаги, у нас получится такой пример:

import (    "context"    "errors"    "net/http"    "testing"    "time"      "github.com/ozontech/cute"    "github.com/ozontech/cute/asserts/headers"    "github.com/ozontech/cute/asserts/json" )   func TestExampleTest(t *testing.T) {     cute.NewTestBuilder().        Title("TestExample_OneStep").        Tags("one_step", "some_local_tag", "json").        Feature("some_feature").        Epic("some_epic").        Description("some_description").        CreateWithStep().        StepName("Example GET json request").        AfterExecuteT(func(t cute.T, resp *http.Response, errs []error) error {            if len(errs) != 0 {                return nil            }            /* Необходимо добавить реализацию */            return nil        },        ).        RequestBuilder(            cute.WithHeaders(map[string][]string{                "some_header":       []string{"something"},                "some_array_header": []string{"1", "2", "3", "some_thing"},            }),            cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"),            cute.WithMethod(http.MethodGet),        ).        ExpectExecuteTimeout(10*time.Second).        ExpectStatus(http.StatusOK).        AssertBody(            json.Equal("$[0].email", "Eliseo@gardner.biz"),            json.Present("$[1].name"),            json.NotPresent("$[1].some_not_present"),            json.GreaterThan("$", 3),            json.Length("$", 5),            json.LessThan("$", 100),            json.NotEqual("$[3].name", "kekekekeke"),              // Custom assert body            func(bytes []byte) error {                if len(bytes) == 0 {                    return errors.New("response body is empty")                }                  return nil            },        ).        AssertBodyT(            func(t cute.T, body []byte) error {                /* Здесь должна быть реализация с T */                return nil            },        ).        AssertHeaders(            headers.Present("Content-Type"),        ).        AssertResponse(            func(resp *http.Response) error {                if resp.ContentLength == 0 {                    return errors.New("content length is zero")                }                  return nil            },        ).        ExecuteTest(context.Background(), t) } 

Allure:

Милый, принеси нам итоги, мы дочитали статью

В Go активно развивается культура тестирования. Не многие компании готовы к экспериментам — пробовать Go в QA. Мы в Ozon пошли на это и не пожалели: удобно, когда разработчики и тестировщики общаются на одном языке и разработчик может отревьюить или поправить автотесты. 

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


Если вас заинтересовало QA на Go, приходите на наш бесплатный курс «Автоматическое тестирование веб-сервисов на Go» — это 52 часа теории и практики от экспертов Ozon. Следующий набор стартует в августе.


ссылка на оригинал статьи https://habr.com/ru/articles/672678/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *