Всем привет! Меня всё так же зовут Сергей, я разработчик в Ozon. 

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

В этой статье речь пойдёт про новые возможности CUTE:

  1. Построение multistep-тестов.
    Рассмотрим, как можно сделать тест, состоящий из нескольких шагов, как достать данные из одного теста и перенести их в другой и как это всё выглядит в Allure.

  2. Загрузка файлов и построение multipart-тесты.
    Один из популярных кейсов — когда при проверке ручки регистрации нужно убедиться, что API может принимать картинки и информацию о пользователе в одном запросе. Рассмотрим, как такое тестировать.

  3. Написание табличных тестов.
    Рассмотрим возможность создавать массивы тестов с проверками, параметризацией и 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 старается не отставать от трендов и развиваться. За год многое внутри библиотеки поменялось, но все изменения мы делаем только на благо пользователям. Если у вас есть идеи, как дополнить, улучшить проект или просто какие-то мысли о нём, поделитесь.

Рекомендую к прочтению небольшую историю про становление нашей команды тестирования. Также отдельно выделю статьи:

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