Привет! Меня зовут Сергей, я старший разработчик в 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. Следующий набор стартует в августе.

Комментарии (4)


  1. sedyh
    22.06.2022 12:17

    Чем это отличается от ginkgo+gomega+agouti?


    1. siller174 Автор
      23.06.2022 11:00
      +1

      Привет.
 Инструменты, которые ты указал, являются более широконаправлеными.

      Здесь же получился инструмент исключительно для тестирования http-сервисов на более простом языке. Безусловно, использовать эти инструменты, то может получится такой же результат, но он будет сложнее. А если попробовать это обвязать Allure, то сложность еще увеличится.

      В пункте «Как было раньше?» я указал ссылку на статью, чтобы можно было сравнить сложность использования.

 А нам хотелось делать тесты проще — откуда и название Create Your Tests Easily.


  1. kostyaBro
    24.06.2022 07:43

    Получится ли этой библиотекой описать сценарий из нескольких запросов? Выглядит инструмент весьма интересно.


    1. siller174 Автор
      24.06.2022 11:31
      +1

      Привет. Да, можно сделать тест состоящий из нескольких запросов.

      Пример:

      import (
      	"context"
      	"net/http"
      
      	"github.com/ozontech/allure-go/pkg/framework/provider"
      	"github.com/ozontech/cute"
      )
      
      func (i *ExampleSuite) TestExample_TwoSteps(t provider.T) {
      	cute.NewTestBuilder().
      		Title("TestExample_TwoSteps").
      		CreateWithStep(). // Создание 1 шага
      		StepName("Step 1").
      		RequestBuilder(
      			cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"),
      			cute.WithMethod(http.MethodPost),
      		).
      		ExpectStatus(http.StatusCreated).
      		ExecuteTest(context.Background(), t).
      		NextTestWithStep(). // Создание 2 шага
      		StepName("Step 2").
      		RequestBuilder(
      			cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"),
      			cute.WithMethod(http.MethodDelete),
      		).
      		ExecuteTest(context.Background(), t)
      }