Go, Allure, and HTTP, or How to Cutely Test HTTP Services in Go (Part 2)

Short description

The CUTE testing library for Go has new features such as testing multiple steps, downloading files and building a multipart test. To show how easy it is to use, developer Sergey provides an example of creating two unrelated tests with the same Allure labels. CUTE allows the developer to code in two different ways, either through the builder or by filling in a structure. The previous article on HTTP testing in Go with CUTE explains the basics for creating E2E tests.

Go, Allure, and HTTP, or How to Cutely Test HTTP Services in Go (Part 2)

Hello everybody! My name is also Sergey, I am a developer at Ozon.

Six months have passed since as long as I can’t find socks It’s been almost a year since my first article on testing HTTP services in Go with the CUTE library, so I’m dying to tell you how you can test HTTP services in Go these days.

This article will cover the new features of CUTE:

  1. Construction of multistep tests.
    Let’s take a look at how you can make a test that consists of several steps, how to get data from one test and transfer it to another, and how it looks in Allure.

  2. Downloading files and building a multipart test.
    One of the popular cases is when, when checking the registration handle, you need to make sure that the API can accept images and information about the user in one request. Let’s consider how to test it.

  3. Writing tabular tests.
    Consider creating test suites with validations, parameterization, and Allure reports.

And many other features. Are you ready? Let’s read it again!

About the basics of creating E2E tests in Go with CUTE, such as:

  • working with Allure tags,

  • forming a request,

  • writing After/Before handlers,

  • creating assertions.

And other important details were discussed in the previous article. I recommend that you study it first, as it will expand the basic knowledge in the field of testing HTTP services.

Let’s start with what? From the beginning!

Let’s start with what? From the beginning!

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", "[email protected]"),
			json.Present("$[1].name"),
		).
		ExecuteTest(context.Background(), t)
}

As a result, we will receive the following report:

Nothing has changed in a year. You can also find all the information to reproduce the request.

I also note that the following information will be in the logs:

=== 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 [email protected], but actual [email protected]
    cute.go:134: Test finished Title
--- FAIL: TestExample (0.13s)

We considered the simplest test with a minimum amount of information, checks and without any additions.

But what if we need to load some file in the test or just use multipart?

Multipart. Dude, let’s download the files shall we?

In version 0.1.10, a constructor was added to create multipart requests.

SupposeYou need to test the handle with two forms, one of which accepts JSON and the other a file.

In principle, it can be done the old-fashioned way.

Sending a file
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)
}

It will work. You can create a few methods to hide the implementation, and then tie Allure to reports and other things. In a word, it’s difficult.

Of course, it’s not as difficult as finding socks, but let’s try to do the same with 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)
}

A query equivalent to the following is executed:

curl -X POST \
     -F "body={\"name\": \"Vasya\"}" \
     -F "image=@/vasya/thebestmypicture.png" \
     http://localhost:7000/v1/banner

And it will be checked that the service has restored 200 (OK).

Multistep test. How to write a test that consists of several queries?

There are situations when multiple queries need to be executed in a test. Let’s try to compile such a test.

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", "[email protected]"), // Проверяем, что в ответе есть поле 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)
}

As a result, we will have two queries executed — and we will receive the following report:

In fact, we took the code from the first section and added it NextTest() and wrote another request.

But I think you paid attention to AfterTestExecuteIn which we took the field from the body of the response of the first request and used it already in the second request.

We could also use AfterTestExecuteTwhich differs only in that it has cute.T logging information. For example, with its help, we can log some header from the response body.

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
}

You can read more about the analogs and capabilities of this block in the previous article in the section “Step 2. Remember the past, do not forget the future.”

Boy, let’s go without a designer!

If you look in the source code of the library, you will find that there is a structure Testwhich allows you to do all the same things that we did before through the builder, only through the filling of the structure.

It looks like this:

type Test struct {
	httpClient *http.Client

	Name string                 // Название теста

	AllureStep *AllureStep      // Allure-теги
	Middleware *Middleware      // After/Before
	Request    *Request         // Запрос
	Expect     *Expect          // Валидация
}

Let’s try to pass the test.

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", "[email protected]"),
				json.Present("$[1].name"),
			},
		},
	}

	test.Execute(context.Background(), t)
}

As a result, we will perform an HTTP GET request, and then make sure that response code = 200 (ОК)and there are fields in the response body email and name.

The report for Allure will still appear, but will be abbreviated without any labels:

Array/table tests. Boy, let’s go without a designer, but let there be a lot of tests!

In the last section, we considered the possibility of creating a simple test without a special attachment to Allure.

But what if we want to use this approach with adding different kinds of labels to the test and have many tests? Let’s try to implement it!

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", "[email protected]"),
					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)
  }

As a result, we created two unrelated tests and in Allure we will have the following:

Both tests will share Allure labels.

Result. Boy, let’s sum up! We want to code!

Can you imagine? I never found the socks, soon to retire, and the library is already a year old. Just kidding.

Testing in Go is gaining momentum. Vacancies for Go testers began to appear. Number of Go projects and tests with CUTE and without it has noticeably increased not only within Ozon, but also in general.

CUTE tries to keep up with trends and develop. During the year, a lot has changed inside the library, but we make all changes only for the benefit of users. If you have ideas on how to add to, improve the project, or just have some thoughts about it, please share.

I recommend reading a short story about the formation of our testing team. I will also single out the following articles:

Related posts