Go, Allure and HTTP, or How nice it is to test HTTP services on Go

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().
		RequestBuilder( // Создаём HTTP-запрос
			cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"),
			cute.WithMethod(http.MethodGet),
		).
		ExpectExecuteTimeout(10*time.Second). // Указываем, что запрос должен выполниться за 10 секунд 
		ExpectStatus(http.StatusOK).          // Ожидаем, что ответ будет 200 (OK)
		AssertBody(                           // Задаём проверку JSON в response body по определённым полям
			json.Equal("$[0].email", "super@puper.biz"),
			json.Present("$[1].name"),
		).
		ExecuteTest(context.Background(), t)
}

And if there are any problems in the test, then the report will be like this:

At the same time, we supported all possible Allure tags and labels.

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

Interested? Then let’s look at all the features of the library.

Building a test

Step 0. Start started

Everything starts somewhere. Our test begins with the preparation of a service that will later help us create tests cute.NewHTTPTestMakerSuite(opts ...Option)

	testMaker := cute.NewHTTPTestMakerSuite(opts ...Option)

At initialization testMaker you can specify basic settings for tests, such as an HTTP client, if this is important to you. Or you can not configure anything – and everything will work out of the box.

Further testMaker will come in handy throughout the entire path of creating tests. Therefore, if you are creating a package / test suite, then I advise you to save testMaker into a common structure for tests. The implementation of such tests can be found in project repositories.

Next, you need to initialize the builder itself, it already needs to be created for each test, since it contains all the information about the test.

	testBuilder := testMaker.NewTestBuilder()

If settings are not important to you, then you can use the prepared builder:

	cute.NewTestBuilder()

Great, on this step 0 finished! And we are ready to start creating the test.

Step 1. Inform everyone

As is usually the case, we need to provide detailed information about the test in order not to forget its purpose in the future. The benefit of Allure allows you to do this, and we can add a lot of information about the test:

	cute.NewTestBuilder().
		Title("TestExample_Simple"). // Заголовок
		Tags("simple", "some_local_tag" ,"some_global_tag", "json"). // Теги для поиска
		Feature("some_feature"). // Фича
		Epic("some_epic"). // Эпик
		Description("some_description"). // Описание теста

In the example above, not all labels are indicated, you can get acquainted with all the others within the project.

Now information about the test will appear in our reports.

Step 2. Remember the past, don’t forget the future

Sometimes there are situations when you need to pre-prepare a request or perform some actions after passing the test.

You can do this outside of the test, but methods are also provided to make it easier:

	BeforeExecute(func(req *http.Request) error)
	AfterExecute(func(resp *http.Response, errs []error) error)

These methods not requiredso you can skip them.

Example:

	cute.NewTestBuilder().
		Create().
		BeforeExecute(func(req *http.Request) error {
			/* Необходимо добавить реализацию */
			return nil
		}).
		AfterExecute(func(resp *http.Response, errs []error) error {
			/* Необходимо добавить реализацию */
			return nil
		})

There is also AfterExecuteT and BeforeExecuteTwhich allow you to add information to Allure, such as creating a new step:

func BeforeExample(t cute.T, req *http.Request) error {
	t.WithNewStep("insideBefore", func(stepCtx provider.StepCtx) {
		now := time.Now()
		stepCtx.Logf("Test. Start time %v", now)
		stepCtx.WithNewParameters("Test. Start time", now)
		time.Sleep(2 * time.Second)
	})
      
	return nil
}

allure:

Step 3. Create a request

There are two ways to pass a request in a test.

If you have already created *http.Requestyou can just use Request(*http.Request):

	req, _ := http.NewRequest(http.MethodGet, "http://google.com", nil)

	cute.NewTestBuilder().
		Create().
		Request(req) // Передача запроса 

The second method is suitable if you do not have a prepared query. Need to call RequestBuilder and independently collect the request.

It looks something like this:

	cute.NewTestBuilder().
		Create().
		RequestBuilder(  // Создание запроса 
			cute.WithHeaders(map[string][]string{ 
				"some_header":       []string{"something"},
				"some_array_header": []string{"1", "2", "3", "some_thing"},
		}),
		cute.WithURI("http://google.com"), 
		cute.WithMethod(http.MethodGet),
   )

All data is displayed in Allure, so anyone without diving into the code can repeat the request.

Great, the most boring is over! We’ve filled in the test information, prepared additional steps, and created a request – it’s time for reviews!

Step 4. Trust, but verify!

I want to note that all checks not required and you are free to choose which ones you need. I’ll show you all the options.

response code

Let’s start with the elementary – by checking the response code. This will help us ExpectStatus(int).

Example:

	cute.NewTestBuilder().
		Create().
		Request(*http.Request). // Передача запроса 
		ExpectStatus(201) // Ожидаем, что ответ будет 201 (Created)

allure:

JSON Schema

Let’s move on to a simple one – to check the JSON schema. It is important for many to always have a clear response scheme.

Exists three ways validate json schema. It all depends on where you have it.

  1. ExpectJSONSchemaString(string) – gets and compares the JSON schema from the string.

  2. ExpectJSONSchemaByte([]byte) – Gets and compares a JSON schema from an array of bytes.

  3. ExpectJSONSchemaFile(string) – receives and compares a JSON schema from a file or a remote resource.

Example:

	cute.NewTestBuilder().
		Create().
		Request(*http.Request). // Передача запроса 
		ExpectJSONSchemaFile("file://./project/resources/schema.json"). // Проверка response body по JSON schema

In case of an error, the report in Allure will be as follows:

Great, the simplest checks are over. It’s time to test our requests for real!

Step 5. I said we need real checks!

It’s time to create real assertions and fully test our response.

The library has prepared asserts for checking headers, for working with JSON in the response body, but you can also create your own.

There are three types of checks:

  1. Checking the response body
    AssertBody and AssertBodyT

  2. Checking response headers
    AssertHeaders and AssertHeadersT

  3. Full check of the response (response)
    AssertResponse and AssertResponseT

Let’s consider in more detail.

Checking the response body (AssertBody)

There are several ready-made solutions in the library for checking the response body.
Consider an example: let’s say we need to get the “email” field from JSON and make sure that the value is equal to “lol@arbidol.com”:

{
  "email": "lol@arbidol.com"
}

To do this, we use the prepared assertion from package:

	cute.NewTestBuilder().
		Create().
		Request(*http.Request). // Передача запроса
		AssertBody(json.Equal("$.email", "lol@arbidol.com")) // Валидация поля “email” в response body

And this is just one of the examples. AT package there is a whole set of assertions:

  • Contains

  • Equal

  • NotEqual

  • Length

  • GreaterThan

  • LessThan

  • Present

  • NotPresent

If the assertion fails, Allure will display a beautiful result:

Checking response headers (AssertHeaders)

Headers are the same story as the response body. Nessesary to use:

	AssertHeaders(func(headers http.Headers) error)
	AssertHeadersT(func(t cute.T, headers http.Headers) error)

There are several ready-made assertions:

Example:

	cute.NewTestBuilder().
		Create().
		Request(*http.Request). // Передача запроса   
		AssertHeaders(headers.Present("Content-Type")) // Проверка, что в заголовках есть “Content-Type”

Full Response Validation (AssertResponse)

When to check at the same time headers, body and something else from the structure http.Responsefit:

   AssertResponse(func(resp *http.Response) error)
   AssertResponseT(func(t cute.T, resp *http.Response) error)

Example:

func CustomAssertResponse() cute.AssertResponse {
	return func(resp *http.Response) error {
		if resp.ContentLength == 0 {
			return errors.New("content length is zero")
		}
 
		return nil
	}
}

Step 6. I want to be independent!

AT 5 step we looked at ready-made assertions and what types exist. But it happens that you want to create something of your own and write your own assertion.

To do this, you need to create a function that will implement one of the types:

  • type AssertBody func(body []byte) error

  • type AssertHeaders func(headers http.Header) error

  • type AssertResponse func(response *http.Response) error

  • type AssertBodyT func(t cute.T, body []byte) error

  • type AssertHeadersT func(t cute.T, headers http.Header) error

  • type AssertResponseT func(t cute.T, response *http.Response) error

Example:

func customAssertHeaders() cute.AssertHeaders {
	return func(headers http.Header) error {
		if len(headers) == 0 {
			return errors.New("response without headers")
		}
 
		return nil
	}
}

There are also functions c cute.Twhich allow you to add information to Allure, such as creating a new step:

func customAssertBody() cute.AssertBodyT {
   return func(t cute.T, body []byte) error {
       step := allure.NewSimpleStep("Custom assert step")
       defer func() {
           t.Step(step)
       }()
 
       if len(body) == 0 {
           step.Status = allure.Failed
           step.WithAttachments(allure.NewAttachment("Error", allure.Text, []byte("response body is empty")))
 
           return nil
       }
 
       return nil
}

And then just add your assertion to the test:

	cute.NewTestBuilder().
		Create().
		Request(*http.Request). // Передача запроса
		AssertHeaders(customAssertHeaders).
		AssertBodyT(customAssertBody)

custom error

You may have noticed that there are a set of fields in the results of prepared assertions: Name, Error, Action and Expected.

To add such data to your custom assertions, you can use:

	cuteErrors.NewAssertError(name string, err string, actual interface{}, expected interface{}) error

Example:

import (
   cuteErrors "github.com/ozontech/cute/errors"
)
 
func customErrorExample (t cute.T, headers http.Header) error {
   return cuteErrors.NewAssertError("custom_assert", "example custom assert", "empty", "not empty")    // Пример создания красивой ошибки 
}

Accordingly, such beauty will appear in Allure:

optional assertions

Sometimes it is necessary to add a check, if it is not performed, the test will not be considered failed.

import (
   cuteErrors "github.com/ozontech/cute/errors"
)

func customOptionalError(body []byte) error { 
   return cuteErrors.NewOptionalError(errors.New("some optional error from creator")) // Пример создания опциональной ошибки 
}

Step 7. Final

To create the test itself, we need to call ExecuteTest(context.Context, testing.TB).

AT ExecuteTest you can pass normal testing.T or provider.T. If you are using allure-goit doesn’t matter – the report will still be generated.

As a result, if we combine all the steps, we get the following example:

import (
   "context"
   "errors"
   "net/http"
   "testing"
   "time"
 
   "github.com/ozontech/cute"
   "github.com/ozontech/cute/asserts/headers"
   "github.com/ozontech/cute/asserts/json"
)
 
func TestExampleTest(t *testing.T) { 
   cute.NewTestBuilder().
       Title("TestExample_OneStep").
       Tags("one_step", "some_local_tag", "json").
       Feature("some_feature").
       Epic("some_epic").
       Description("some_description").
       CreateWithStep().
       StepName("Example GET json request").
       AfterExecuteT(func(t cute.T, resp *http.Response, errs []error) error {
           if len(errs) != 0 {
               return nil
           }
           /* Необходимо добавить реализацию */
           return nil
       },
       ).
       RequestBuilder(
           cute.WithHeaders(map[string][]string{
               "some_header":       []string{"something"},
               "some_array_header": []string{"1", "2", "3", "some_thing"},
           }),
           cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"),
           cute.WithMethod(http.MethodGet),
       ).
       ExpectExecuteTimeout(10*time.Second).
       ExpectStatus(http.StatusOK).
       AssertBody(
           json.Equal("$[0].email", "Eliseo@gardner.biz"),
           json.Present("$[1].name"),
           json.NotPresent("$[1].some_not_present"),
           json.GreaterThan("$", 3),
           json.Length("$", 5),
           json.LessThan("$", 100),
           json.NotEqual("$[3].name", "kekekekeke"),
 
           // Custom assert body
           func(bytes []byte) error {
               if len(bytes) == 0 {
                   return errors.New("response body is empty")
               }
 
               return nil
           },
       ).
       AssertBodyT(
           func(t cute.T, body []byte) error {
               /* Здесь должна быть реализация с T */
               return nil
           },
       ).
       AssertHeaders(
           headers.Present("Content-Type"),
       ).
       AssertResponse(
           func(resp *http.Response) error {
               if resp.ContentLength == 0 {
                   return errors.New("content length is zero")
               }
 
               return nil
           },
       ).
       ExecuteTest(context.Background(), t)
}

allure:

Honey, bring us the results, we have finished reading the article

Go has a strong culture of testing. Not many companies are ready to experiment – to try Go in QA. We at Ozon went for it and did not regret it: it is convenient when developers and testers communicate in the same language and the developer can review or correct autotests.

I hope our cute library you will need. You can study on your own examples and if you have any questions, ask them in the comments.


If you are interested in QA on Go, come to our free course “Automated testing of web services in Go” is 52 hours of theory and practice from Ozon experts. The next set starts in August.

Similar Posts

Leave a Reply