Привет! Меня зовут Сергей, я старший разработчик в 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-сервисов, который в итоге перерос в библиотеку.
Как было раньше?
Обычно тест состоит из следующих шагов:
Подготовить HTTP-клиент.
Создать данные для теста.
Выполнить HTTP-запрос.
Убедиться, что запрос выполнился.
Считать ответ в структуру.
Начать проверять структуру.
Если вы хотите ещё всё обернуть в 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-схему. Всё зависит от того, где она у вас находится.
ExpectJSONSchemaString(string)
— получает и сравнивает JSON-схему из строки.ExpectJSONSchemaByte([]byte)
— получает и сравнивает JSON-схему из массива байтов.ExpectJSONSchemaFile(string)
— получает и сравнивает JSON-схему из файла или удалённого ресурса.
Пример:
cute.NewTestBuilder().
Create().
Request(*http.Request). // Передача запроса
ExpectJSONSchemaFile("file://./project/resources/schema.json"). // Проверка response body по JSON schema
В случае ошибки отчёт в Allure будет таким:
Отлично, самые простые проверки позади. Пора проверить наши запросы по-настоящему!
Шаг 5. Я сказал, нам нужны настоящие проверки!
Настало время создать настоящие ассерты и проверить полностью наш response.
В библиотеке есть подготовленные ассерты для проверки заголовков, для работы с JSON в теле ответа, но также вы можете создавать свои.
Существует три типа проверок:
Проверка тела ответа (response body)
AssertBody
иAssertBodyT
Проверка заголовков ответа (response headers)
AssertHeaders
иAssertHeadersT
Полная проверка ответа (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)
kostyaBro
24.06.2022 07:43Получится ли этой библиотекой описать сценарий из нескольких запросов? Выглядит инструмент весьма интересно.
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) }
sedyh
Чем это отличается от ginkgo+gomega+agouti?
siller174 Автор
Привет. Инструменты, которые ты указал, являются более широконаправлеными.
Здесь же получился инструмент исключительно для тестирования http-сервисов на более простом языке. Безусловно, использовать эти инструменты, то может получится такой же результат, но он будет сложнее. А если попробовать это обвязать Allure, то сложность еще увеличится.
В пункте «Как было раньше?» я указал ссылку на статью, чтобы можно было сравнить сложность использования. А нам хотелось делать тесты проще — откуда и название Create Your Tests Easily.