Всем привет! Меня всё так же зовут Сергей, я разработчик в 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 старается не отставать от трендов и развиваться. За год многое внутри библиотеки поменялось, но все изменения мы делаем только на благо пользователям. Если у вас есть идеи, как дополнить, улучшить проект или просто какие-то мысли о нём, поделитесь.
Рекомендую к прочтению небольшую историю про становление нашей команды тестирования. Также отдельно выделю статьи: