Всем привет! Меня всё так же зовут Сергей, я разработчик в Ozon.
Прошло полгода с тех пор, как я не могу найти носки выхода моей первой статьи про тестирование HTTP-сервисов на Go, уже почти год библиотеке CUTE, поэтому я горю желанием рассказать вам, как нынче можно тестировать HTTP-сервисы на Go.

В этой статье речь пойдёт про новые возможности CUTE:
Построение multistep-тестов.
Рассмотрим, как можно сделать тест, состоящий из нескольких шагов, как достать данные из одного теста и перенести их в другой и как это всё выглядит в Allure.Загрузка файлов и построение multipart-тесты.
Один из популярных кейсов — когда при проверке ручки регистрации нужно убедиться, что API может принимать картинки и информацию о пользователе в одном запросе. Рассмотрим, как такое тестировать.Написание табличных тестов.
Рассмотрим возможность создавать массивы тестов с проверками, параметризацией и Allure-отчётами.
И много других фич. Готовы? Let's read it again!
О базовых вещах при создание E2E-тестов на Go с помощью CUTE, таких как:
работа с Allure-тегами,
формирование запроса,
написание After/Before обработчиков,
cоздание асертов.
И других важных мелочах рассказывалось в предыдущей статье. Рекомендую сначала изучить её, так как она расширит базовые знания в области тестирования HTTP-сервисов.
Начнём с чего? С начала!
Начнём с чего? С начала!
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(). RequestRepeat(3). // В случае если response.status != 200 (OK), запрос будет отправлен ещё раз RequestBuilder( // Создаём HTTP-запрос cute.WithHeadersKV("x-auth", "hello, my friend!"), cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodGet), ). ExpectExecuteTimeout(10*time.Second). // Указываем, что запрос должен выполниться за десять секунд ExpectStatus(http.StatusOK). // Ожидаем, что ответ будет 200 (OK) AssertBody( // Задаём проверку JSON в теле ответа по определенным полям json.Equal("$[0].email", "hello-my-friend@puper.biz"), json.Present("$[1].name"), ). ExecuteTest(context.Background(), t) }
В результате мы получим следующий отчёт:

За год ничего не изменилось. Вы всё так же можете найти всю информацию для воспроизведения запроса.
Также замечу, что в логах будет следующая информация:
=== RUN TestExample cute.go:131: Test start Title test.go:267: Start make request step_context.go:100: [Request] curl -X 'GET' -d '' -H 'x-auth: hello, my friend!' 'https://jsonplaceholder.typicode.com/posts/1/comments' step_context.go:100: [Response] Status: 200 OK test.go:275: Finish make request common.go:123: [ERROR] on path $[0].email. expect super@puper.biz, but actual Eliseo@gardner.biz cute.go:134: Test finished Title --- FAIL: TestExample (0.13s)
Мы рассмотрели самый простой тест с минимальным количеством информации, проверок и без каких-либо дополнений.
Но что, если нам нужно в тесте загрузить какой-то файл или просто использовать multipart?
Multipart. Парень, давай загрузим файлы?
В версию 0.1.10 был добавлен конструктор для создания multipart-запросов.
Предположим, вам нужно протестировать ручку с двумя формами, в одной из которых она принимает JSON, а в другой — файл.
В принципе это можно сделать по старинке.
Отправка файла
import ( "net/http" "os" "bytes" "path" "path/filepath" "mime/multipart" "io" ) func main() { fileDir, _ := os.Getwd() fileName := "file.txt" filePath := path.Join(fileDir, fileName) file, _ := os.Open(filePath) defer file.Close() body := &bytes.Buffer{} writer := multipart.NewWriter(body) part, _ := writer.CreateFormFile("file", filepath.Base(file.Name())) io.Copy(part, file) writer.Close() r, err := http.NewRequest("POST", "http://example.com", body) if err != nil { panic(err) } r.Header.Add("Content-Type", writer.FormDataContentType()) client := &http.Client{} client.Do(r) }
Это будет работать. Можно создать несколько методов, чтобы спрятать реализацию, а потом ещё обвязать Allure отчетами и другими вещами. Одним словом тяжело....
Конечно, по сложности это не сравнится с поиском носков, но давайте попробуем то же самое сделать с помощью CUTE.
import ( "context" "testing" "github.com/ozontech/cute" ) func TestUploadfile(t *testing.T) { cute.NewTestBuilder(). Title("Uploat file"). Create(). RequestBuilder( cute.WithURI("http://localhost:7000/v1/banner"), cute.WithMethod("POST"), cute.WithFormKV("body", []byte("{\"name\": \"Vasya\"}")), // Заполняем текстовую форму cute.WithFileFormKV("image", &cute.File{ // Заполняем форму с файлом Path: "/vasya/thebestmypicture.png", }), ). ExpectStatus(http.StatusOK). ExecuteTest(context.Background(), t) }
Выполнится запрос, эквивалентный следующему:
curl -X POST \ -F "body={\"name\": \"Vasya\"}" \ -F "image=@/vasya/thebestmypicture.png" \ http://localhost:7000/v1/banner
И будет проверено, что сервис вернул 200 (OK).
Multistep-тест. Как написать тест, состоящий из нескольких запросов?
Бывают ситуации, когда в тесте необходимо выполнить несколько запросов. Давайте попробуем собрать такой тест.
import ( "context" "fmt" "io" "net/http" "testing" "time" "github.com/ozontech/cute" "github.com/ozontech/cute/asserts/json" ) // Структура запроса на удаление type deleteRequest struct { Email string `json:"email"` } func Test_TwoSteps(t *testing.T) { dRequest := &deleteRequest{} // Подготавливаем структуру запроса для удаления cute.NewTestBuilder(). Title("Создание и удаление комментария"). Tags("comments"). // Подготавливаем запрос на создание CreateStep("Create comment /posts/1"). RequestBuilder( // Создаём HTTP-запрос, который будет отправлен cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodGet), cute.WithHeadersKV("some_auth_token", “auth-value”), ). ExpectExecuteTimeout(10*time.Second). ExpectStatus(http.StatusOK). AssertBody( json.Equal("$[0].email", "Eliseo@gardner.biz"), // Проверяем, что в ответе есть поле email ). NextTest(). AfterTestExecute( func(response *http.Response, errors []error) error { b, err := io.ReadAll(response.Body) if err != nil { return err } temp, err := json.GetValueFromJSON(b, "$[0].email") // Получаем email из тела ответа if err != nil { return err } dRequest.Email = fmt.Sprint(temp) // Сохраняем email return nil }, ). // Подготавливаем запрос на удаление CreateStep("Delete comment"). RequestBuilder( cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodDelete), cute.WithMarshalBody(dRequest), cute.WithHeadersKV("some_auth_token", fmt.Sprint(11111)), ). AssertBody( json.Present("$[0].email"), ). ExecuteTest(context.Background(), t) }
В итоге у нас будут выполнены два запроса — и мы получим следующий отчёт:

По факту мы взяли код из самого первого раздела, добавили NextTest() и написали ещё один запрос.
Но думаю, вы обратили внимание на AfterTestExecute, в котором мы достали из тела ответа первого запроса поле и использовали его уже во втором запросе.
Также мы могли использовать AfterTestExecuteT, который отличается лишь тем, что имеет cute.T для логирования информации. Например, с помощью него мы можем залогировать какой-нибудь заголовок из тела ответа.
func (t cute.T, response *http.Response, errors []error) error { t.Logf("[request_info] Trace_id - %v", response.Header.Get("x-trace-id")) return nil }
Подробнее про аналоги и возможности этого блока можно прочитать в прошлой статье в разделе «Шаг 2. Помни о прошлом, не забывай о будущем».
Парень, давай без конструктора!
Если вы заглянете в исходный код библиотеки, то обнаружите, что есть структура Test, которая позволяет сделать всё то же самое, что мы делали ранее через билдер, только через заполнение структуры.
Это выглядит следующим образом:
type Test struct { httpClient *http.Client Name string // Название теста AllureStep *AllureStep // Allure-теги Middleware *Middleware // After/Before Request *Request // Запрос Expect *Expect // Валидация }
Давайте попробуем составить тест.
func Test_One_Execute(t *testing.T) { test := &cute.Test{ Name: "test_1", // Название теста Request: &cute.Request{ // Собираем запрос Builders: []cute.RequestBuilder{ cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodGet), }, }, Expect: &cute.Expect{ // Добавляем валидацию Code: 200, AssertBody: []cute.AssertBody{ json.Equal("$[0].email", "Eliseo@gardner.biz"), json.Present("$[1].name"), }, }, } test.Execute(context.Background(), t) }
В итоге мы выполним HTTP GET-запрос, а далее убедимся, что response code = 200 (ОК), а в теле ответа есть поля email и name.
Отчёт для Allure появится всё равно, но будет сокращённым — без каких-либо лейблов:

Array/table-тесты. Парень, давай без конструктора, но чтобы было много тестов!
В прошлом разделе мы рассмотрели возможность создания простого теста без особой привязки к Allure.
Но что, если нам хочется использовать такой подход с добавлением разного рода лейблов в тест и чтобы тестов было много? Давайте попробуем это реализовать!
func Test_array(t *testing.T) { tests := []*cute.Test{ { Name: "Create something", // Cоздаём первый тест Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodPost), }, }, Expect: &cute.Expect{ Code: 201, }, }, { Name: "Delete something", // Cоздаём второй тест Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodGet), }, }, Expect: &cute.Expect{ Code: 200, AssertBody: []cute.AssertBody{ json.Equal("$[0].email", "Eliseo@gardner.biz"), json.Present("$[1].name"), func(body []byte) error { // Создаём свой assert return errors.NewAssertError("example error", "example message", nil, nil) }, }, }, }, } cute.NewTestBuilder(). Tag("table_test"). // Общий тег для двух тестов Description("Common description for array tests") // Общее описание CreateTableTest(). PutTests(tests...). ExecuteTest(context.Background(), t) }
В итоге мы создали два не связанных между собой теста — и в Allure у нас появится следующее:

Оба теста будут иметь общие Allure-лейблы.

Итог. Парень, давай итоги! Мы хотим кодить!
Представляете? Я так и не нашёл носки, скоро на пенсию, а библиотеке уже год. Шучу.
Тестирование в Go набирает обороты. Начали появляться вакансии Go-тестировщиков. Количество проектов и тестов на Go с CUTE и без него, заметно увеличилось не только внутри Ozon, но и в целом.
CUTE старается не отставать от трендов и развиваться. За год многое внутри библиотеки поменялось, но все изменения мы делаем только на благо пользователям. Если у вас есть идеи, как дополнить, улучшить проект или просто какие-то мысли о нём, поделитесь.
Рекомендую к прочтению небольшую историю про становление нашей команды тестирования. Также отдельно выделю статьи:
