Go, Allure и HTTP, или Как мило тестировать HTTP-сервисы на Go (часть 2)

от автора

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/728172/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *