Привет всем. Меня зовут Таня. Я автоматизирую на Go уже около года в компании Vivid Money. До этого занималась 4 года автоматизацией тестов на Java.

В этой статье расскажу:

  • как писала интеграционные тесты на Go

  • с какими проблемами столкнулась

  • с какими библиотеками и инструментами работаю

Эта статья для тех:

  • кто впервые столкнулся с Go, как когда-то я

  • кому интересно, как можно взаимодействовать с Go в тестировании

  • кто не знает, с чего начать

О чем будем говорить:

По терминам

Unit-тестирование (Модульное тестирование) заключается в тестировании этого отдельного модуля.

Интеграционное тестирование отвечает за тестирование интеграции между сервисами.

Allure — это библиотека, которая позволяет формировать отчеты на человекочитаемом языке. В нем есть пошаговая структура с предусловиями и постусловиями. Также можно отслеживать историю прохождения тест-кейсов и статистику. С его помощью классно отчитываться боссам о тестировании =)

Выбор языка Go и Allure

В компании разработка сервисов пишется на нескольких языках: Java, Kotlin, Scala и Go. В моей команде — Go.  Поэтому было решено, что тесты будут написаны тоже на Go, чтобы разработчики могли помочь с автоматизацией инструментов и поддержкой тестов.

В тестах самое главное — это понимание, что уже протестировано, где падает и как падает. Для этого нужны понятные отчеты. У нас развернут allure-сервер, поэтому необходимо было интегрироваться с Allure.

Почему выбрана Allure-go библиотека

Фреймворка от создателей Allure для языка Go, к сожалению, нет (пока нет).

На момент написания тестов я нашла всего две библиотеки для интеграции Allure с Go

Allure-go-common — написана 6 лет назад. И никак не изменялась, а хотелось, чтобы была какая-то поддержка библиотеки в случае чего; отсутствует какая-либо документация.

Allure-go — недавно написана, есть документация с многочисленными примерами и множеством функций.

Собственно, из всего выбрала allure-go по функционалу и удобству работы с ним.

Как выглядит Allure-go

Сравнение с Java

Если кто-то, как я, пришел из Java, то расскажу немного про отличия.

Про структуру кода:

  • Java — понятная структура. Код пишется в src/main/, тесты лежат в src/test/

  • Go — нет четкой структуры, есть только рекомендации. Юнит тесты обычно лежат рядом с пакетом, т.е в папке будет, например, пакет sender.go и рядом будет в этой же папке sender_test.go (по постфиксу _test можно понять, что это файл с тестами)

Как выглядит тест на Java + Allure

Нужно написать аннотацию @Test

@Test
public void sendTransactionSuccеedTest() {
    String clientID = config.getClientID();
    String trxID = apiSteps.sendTransaction(clientID);
    Transaction trx = dbSteps.getTransactionByID(trxID);
    assertSteps.checkTransactionFields(trx);
}

В шаге указываем аннотацию @Step

@Step(“Send transaction with clientID \\d+“)
public void sendTransaction(int clientID) {
    String msg = parseJSON();
    msg.SetClient(clientID);
    s.trxClient.Send(msg);
}

В Go нет аннотаций, как в Java. Чтобы описать шаг, тут не обойдешься @step.

Структура такая же, только вместо аннотации нужно добавить метод allure.step. Внутри step-a нужно добавить описание в методе allure.description, вторым аргументом передается allure.Action — где указывается функция с действием.

func doSomething(){
    allure.Step(allure.Description("Something"), allure.Action(func(){
        doSomethingNested()
    }))
}

Описание Теста с методом allure.Test:

func TestPassedExample(t *testing.T) {
    allure.Test(t,
                allure.Description("This is a test to show allure implementation with a passing test"),
                allure.Action(func() {
                    s := "Hello world"
                    if len(s) == 0 {
                      	t.Errorf("Expected 'hello world' string, but got %s ", s)
                    }
                }))
}

Больше примеров можно найти тут.

Далее опишу, как это выглядит в моих тестах.

Assertion-ы в go, выбор библиотеки

В тестах я использую assertion-ы. Assertion-ы — это функции для сравнения ожидаемых значений с реальными. Функции, с помощью которых мы понимаем, в какой момент упал тест и с какой ошибкой.

Я не стала использовать стандартные библиотеки для проверки значений, потому что в интеграционных сценариях сложные проверки объектов, и подходящая библиотека с assertion-ами облегчит читаемость и сам тест.

Также, одна из важных функций assertion-а в тестах, это неявные (умные) ожидания — когда ожидаешь какое-то событие в течение промежутка времени.

Критерии поиска библиотеки:

  1. Совместимость с Go, Allure

  2. Наличие документации

  3. Умные ожидания

  4. Возможность проверять несколько полей в объектах, сами объекты с разными условиями

  5. Вывод всего объекта, если есть ошибка в одном из полей (soft assertion)

Сравнительная таблица assertion библиотек по этим критериям

Библиотеки

General info

Документация

Cовместимость с Go, Allure

Умные ожидания

Сложные проверки объектов, soft assertion

Go стандартная библиотка

Для подержки автоматизаци тетстирования в Go

Есть

Есть

Нет

Нет

testify

Библиотека с assertion-ами и вспомогательными функциями для моков.

Есть

Есть

Нет

Нет

gocheck

Небольшая библиотека с assertion-ами. Позиционируется, как расширение стандартной библиотеки go test. Имеет похожую функциональность с testify.

Есть

Есть

Нет

Нет

gomega

Библиотека с assertion-ами. Адаптирована под работу с BDD фреймворком Ginkgo. Но может и работать и с другими фреймворками.

Есть

Есть

Есть

Есть

gocmp

Предназначена для сравнения значений. Более мощная и безопасная альтернатива рефлексии reflect.DeepEqual

Есть

Есть

Нет

Нет

go-testdeep

Предоставляет гибкие функции для сложного сравнения значений. Переписан и адаптирован из Test::Deep perl module

Есть

Есть

Нет

Есть

Из всего перечисленного мной была выбрана Gomegа — четко описанная документация с примерами, множество функций для сравнения объектов и, самое главное, функции eventually и consistently, которые позволяют делать умные ожидания.

Также, отмечу, что есть много BDD-тестовых фреймворков, которые адаптированы для go вместо Allure. Например. Gingko, Goconvey, Goblin. И если нет обязательств использовать Allure, то стоит их тоже рассмотреть. Выбирайте библиотеки исходя из своих собственных задач.

Статьи со сравнением тестовых фреймворков

O gomega

Что же такое Gomega?

Пример assertion-a

  • Можно использовать ?:

?(ACTUAL).Should(Equal(EXPECTED))
?(ACTUAL).ShouldNot(Equal(EXPECTED))
  • Или Expect:

Expect(ACTUAL).To(Equal(EXPECTED))
Expect(ACTUAL).NotTo(Equal(EXPECTED))
Expect(ACTUAL).ToNot(Equal(EXPECTED))

Как выглядит в тесте с Allure:

allure.Test(t, allure.Action(func() {
    allure.Step(allure.Description("Something"), allure.Action(func() {
      gomega.Expect(err).ShouldNot(gomega.HaveOccurred(), "Text error")
    }))
}))

Как выглядит асинхронные ассершены (умные ожидания) в Gomega:

Eventually(func() []int {
        return thing.SliceImMonitoring
}, TIMEOUT, POLLING_INTERVAL).Should(HaveLen(2))

Функция eventually запрашивает указанную функцию с частотой (POLLING_INTERVAL), пока возвращаемое значение не будет удовлетворять условию или не истечет таймаут.

Удобно использовать при запросе данных из базы, которые должны появиться в течение какого-то времени. (В разделе "Работа с базой данных" есть пример)

Consistently — функция проверяет, что результат соответствует ожидаемому в течение периода времени c заданной частотой.

Удобно использовать, когда необходимо проверить, что значения не меняются в течение периода. Например, когда в базе после запроса не должно появиться новых строк, или значение не должно меняться в течение 10 секунд.

Пример:

Consistently(func() []int {
        return thing.MemoryUsage()
}).Should(BeNumerically("<", 10))

Как интегрировать gomega с allure

Gomega адаптирована для работы с BDD фреймворком Gingko (нам он не нужен, но в доке описана интеграция с ним, и это поможет нам поменять на allure фреймворк).

В качестве примера в документации описано, что для Gingko надо зарегистрировать обработчик, перед началом тест-сьюта:

gomega.RegisterFailHandler(ginkgo.Fail)

Когда gomega assertion фейлится, gomega вызывает GomegaFailHandler (эта функция вызывается с помощью gomega.RegisterFailHandler())

Мне нужен был Allure, поэтому нужно было написать свой FailHandler.

Какая была проблема. Изначально при внедрении gomega я регистрировала gomega c помощью RegisterTestingT, как на примере ниже

func TestFarmHasCow(t *testing.T) {
    gomega.RegisterTestingT(t) 
    f := farm.New([]string{"Cow", "Horse"})
    gomega.Expect(f.HasCow()).To(BeTrue(), "Farm should have cow")
}

Итог: столкнулась, что в Allure отчете отсутствовали ошибки от gomega. Выводился отчет с тестом, что шаг зафейлился (как на скрине), но причина ошибки не выводилась.

Как исправить. Чтобы добавить метод для обработки ошибок, нужно написать свою функцию wrapper, которая будет вызываться при фейле, и добавить туда метод allure.Fail (вызывает allure.error, сохраняет результат и статус теста — fail, сохраняет стектрейс ошибки)

//наш кастомный wrapper
func BuildTestingTGomegaFailWrapper(t *testing.T) *types.GomegaFailWrapper { 
    //вызов функции fail        
    fail := func(message string, callerSkip ...int) { 
        // добавление вызова allure.fail для выгрузки в отчет ошибки
        allure.Fail(errors.New(message))     
        t.Fatalf("\\n%s %s", message, debug.Stack())
    }
    return &types.GomegaFailWrapper{
        Fail:        fail,
        TWithHelper: t,
    }
}

Перед выполненим теста, нужно указать параметры для gomega и в FailHandler передать наш wrapper.BuildTestingTGomegaFailWrapper(t).Fail

func SetGomegaParameters(t *testing.T) {
		//регистрация T в gomega, чтоб не передавать t внутри методов для тестирования
    gomega.RegisterTestingT(t) 
		//регистрация кастомного обработчика
    gomega.RegisterFailHandler(wrapper.BuildTestingTGomegaFailWrapper(t).Fail)
}

В итоге в отчете появляется ошибка, и теперь понятно, что именно пошло не так.

Также еще хочется добавить, что Gomega в тестах удобна тем, что не нужно в методы передавать везде testing.T.

Про то, как устроены тесты

Для меня в самом начале был большой вопрос как выстроить удобную структуру проекта в Go для тестов. В итоге вот какая структура сформировалась:

  • api — пакеты с proto-файлами

  • pkg — пакет, где лежит весь код для тестов (шаги, которые мы вызываем для выполнения теста и assertion steps)

    • database — работа с базой

    • grpc — работа с сервисами по grpc

    • overall — хранятся верхнеуровневые шаги (если есть длинная последовательность шагов, и она постоянно повторяется, и это мешает читаемости теста, то выносим последовательность этих повторяющихся шагов и объединяем в верхнеуровневый шаг в этот пакет)

    • clients — внутри этих каталогов функционал, работающий с сервисами. Если нужно что-то отправить куда-то и получить ответ от сервиса, это клиент. Клиент берет параметры из конфига при инициализации

    • di — работа с dependency injection, чтоб не прописывать зависимости

    • config — описание работы конфига

    • assertion — проверки, каждый пакет отвечает за свою функциональную часть, если заканчивается на steps, значит там шаги теста (allure.step), если без steps, то там просто унарные проверки.

  • suites — только сами тесты, список и вызов шагов из pkg (пример указан ниже)

  • testdata — данные, используемые в тестах

  • tests — тут лежат файлы, описывающие запуск тестов, которые лежат в suites

    • runner_test.go — разные runner-ы для тестов

    • main_test.go — файл, в котором лежит func TestMain(m *testing.M) {} — это функция, в которой осуществляется запуск тестов с помощью m.Run

  • tmp — папка для выгрузки allure результатов

Как выглядит suite:

Тут два теста. Название, указаное в t.Run, будет указано в allure-отчете c нижним подчеркиванием. Либо можно дополнительно прописать allure.Name в тесте.

func TestRequest(t *testing.T, countryType string) {
    t.Run("Test1: Успешная отправка запроса", func(t *testing.T) {
        allure.Test(t, allure.Action(func() {
          	SendRequestSuccеed(t)
        }))
    })
    t.Run("Test2: Отправка запроса с ошибкой", func(t *testing.T) {
        allure.Test(t, allure.Action(func() {
          	SendingResuestWithErrorTest(t)
        }))
})

Как выглядит тест:

func SendRequestSuccеed(c *di.Components, t *testing.T) { // di c уже проинициализированными компонентами
    common.SetGomegaParameters(t) // присвоение gomega параметров
    clientID := c.Config.GetClientID()
    reqD := c.apiSteps.SendRequest(clientID) // отправка запроса
    req := c.DBSteps.GetRequestByID(reqID) // запрос из базы
    c.assertSteps.CheckRequestFields(req) // проверка полей в базе
}

Что такое шаг? Это выполнение действия для получения какого-то результата. Для каждого метода внутри будет конструкция вида allure.Step(allure.Description(), allure.Action(func() )

Пример шага с библиотекой allure-go

func (s *Steps) SendRequest(clientID string) {
    allure.Step( // определение шага
      allure.Description( // описание шага
          fmt.Sprintf("Send request with clientID: %s", clientID)),
      allure.Action(func() { // функция шага
        	msg := parseJSON() // берем сообщение из шаблона
        	msg.SetClient(clientID)
        	s.reqClient.Send(msg)
    }))
}

Пример Assertion шага

func CheckRequestFields(req Request, expectedStatus req.Status, statusReason req.StatusReason){
  	allure.Step(
      allure.Description("Check fields of request in db"), 
      allure.Action(func() {
      		gomega.Expect(trx.Status).Should(gomega.Equal(expectedStatus), "Status was not expected")
      		gomega.Expect(trx.StatusReason).Should(gomega.Equal(statusReason), "Status reason was not expected")
}

Внутри шага можно добавить еще шаги. Все это при выполнении складывается в отчет, и можно точно отследить, что происходило.

Пример теста в allure-отчете:

Также можно добавить attachment в виде json или текста (как на скрине выше — в Actual fields)

err = allure.AddAttachment(name, allure.*ApplicationJson*, []byte(fmt.Sprintf("%+v", string(aJSON))))
err := allure.AddAttachment(name, allure.*TextPlain*, []byte(fmt.Sprintf("%+v", a)))

Далее идет часть вглубь, я расскажу, как работаю с базой данных, rest-клиентом, конфигами, di и gitlab. Если интересно и еще совсем не заскучали, запаситесь чайком, кофейком)


Работа с REST-запросами

Для выполнения REST-запросов я использую библиотеку go-resty

Для того, чтобы выполнить запрос надо создать resty клиент

client := resty.New()
authJSON, err := json.Marshal(authRequest)
// выполнить запрос Post
resp, err := client.R().
		SetHeader("Content-Type", "application/json").
    SetBody(authJSON). //тело
    Post(c.url) //урл запроса

Создаем структуру Client

type Client struct {
  	log *log.CtxLogger
  	*resty.Client
  	url string
}

При выполнение программы вызывается инициализация клиента, в который передается аргументом лог и конфиг)

func NewClient(l log.Service, conf config.APIConfigResult) *Client {
    return &Client{
        Client: resty.New(), 
        log: l.NewPrefix("Сlient"),
        url: conf.Auth.URL
    }
}

Отправка запроса

func (c *Client) Send(req Request) Response {
    reqJSON, err := json.Marshal(req) //формируем json из объекта
    gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
    resp, err := c.R().
        SetHeader("Content-Type", "application/json").
        SetBody(reqJSON).
        Post(c.url) // поддерживает все типы GET, POST, PUT, DELETE, HEAD, PATCH, OPTIONS
    gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
    c.log.InfofText("Response", resp.String())
    // проверяем что статус 200
    gomega.Expect(resp.StatusCode()).Should(gomega.Equal(http.*StatusOK*)) 
    resp := Response{}
    err = json.Unmarshal(resp.Body(), &resp) // из  json делаем объект  
    gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
    return resp // возвращаем объект ответа
}

Работа с базой данных

Для работы с базами данных использую стандартную библиотеку database/sql. База данных в прокте postgresql.

Для начала работы необходимо указать импорт драйвера pq.

import (
    "database/sql"
    _ "github.com/lib/pq"
)

Создаем структуру Client с *sql.DB

type Client struct {
    *sql.DB
    log *log.CtxLogger
}

Инициализация клиента

func NewClient(conf config.APIConfigResult, l log.Service) *Client {
        //берем из конфига  все параметры  для базы данных
        dbConf := conf.DB
        // создаем строку подключения, она выглядит так
        dbURL := fmt.Sprintf("postgres://%v:%v@%v:%v/%v", dbConf.Username, dbConf.Password, dbConf.Host, dbConf.Port, dbConf.DBName)
        //открываем соединение
        db, err := sql.Open("postgres", dbURL)
        gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
        return &Client{DB: db, log: l.NewPrefix("db.client.cardmanager")}
}

Чтоб сделать запрос в базу (пример с умным ожиданием, ждем пока строка появится в таблице)

//select запрос
const selectCardInfoByClient = "Select status from cards where client_id = $1"

func (c *Client) GetCardStatus(clientID string) (status string) {
    gomega.Eventually(func() error {
        // передаем строку запроса и параметр
        err := c.QueryRow(selectCardInfoByClient, clientID). 
        Scan(&status) // ожидаем статус
        return err
    }, "60s", "1s").
    //если вернулась ошибка, запрашиваем еще раз; как только ошибки нет, возвращаем статус
  			Should(gomega.Succeed(), fmt.Sprintf("Not found card by client: %s in db", clientID)) 
    c.log.InfofJSON("Card info", status)
    return status
}

Работа с конфигом

Все конфигурационные параметры лучше всегда хранить в отдельном файле. У меня есть несколько тестовых окружений, поэтому для каждого свои параметры хранятся в отдельных yaml файлах.

В конфигурационном файле обычно хранят параметры для соединения с базой, урлы, параметры логирования, и т.д

Пример yaml файла.

userService:
	url: "<https://testurl.org>"

dbProс:
  host: dbhost.ru
  username: user
  dbname: userdb
  password: pass
  port: 5432

Далее объявляется структура, чтобы распарсить конфиг в объект

type DB struct {
    Host     string `yaml:"host"`
    Port     string `yaml:"port"`    
    DBName   string `yaml:"dbname"`
    Username string `yaml:"username"`
    Password Secret `yaml:"password"`
}

type Client struct {        
  	URL string `yaml:"url"`
}

И структура самого конфига

type APIConfig struct {
    MainConfig       `yaml:"main"`
    Logger           `yaml:"logger" validate:"required"`
    DBProc     			  DB         `yaml:"dbProс" validate:"required"`
    UserService       Client     `yaml:"clearing" validate:"required"`
}

//результирующий конфиг, который везде будем передавать
type APIConfigResult struct {
    Logger
    DBProcessing     DB
    UserService      Client
}

В параметре запуска указывается переменная env окружения

const (
    LocalEnv  string = "local"
    DevEnv    string = "dev"
    StableEnv string = "stable"
)

func getEnv() (string, error) {
   if env := os.Getenv(Env); len(env) != 0 {
       switch env {
       caseDevEnv:
         	return DevEnv, nil
       caseLocalEnv:
         	return LocalEnv, nil
       caseStableEnv:
         	return StableEnv, nil
       }
   }
   return "", fmt.Errorf("cannot parse env variable")
}

Создание APIConfigResult на основе yaml конфига

func NewAPIConfig() (APIConfigResult, error) {
    var (
        c      APIConfig
        result APIConfigResult
        err    error
    )
  	c.MainConfig.Env, err = getEnv() ///выбор окружения

    if err != nil {
      	return APIConfigResult{}, err
    }

    var confPath string

    switch c.MainConfig.Env { //выбор конфига в зависимости от окружения
        case StableEnv:
      			confPath = "config-stable.yml"
        case DevEnv:
        		confPath = "config-dev.yml"
        default:
        		confPath = "config-local.yml"
    }

	  //чтение конфига и запись в 'c' объект
    if err := ReadConfig(confPath, &amp;c); err != nil {
      	return result, errors.Wrap(err, `failed to read config file`)
    }

    result.Logger = c.Logger
    result.DBProcessing = c.DBProcessing
    result.UserService = c.UserService

    return result, validator.New().Struct(c)

}
func ReadConfig(configPath string, config interface{}) error {
    if configPath == `` {
     	 return fmt.Errorf(no config path)
    }
  	//чтение файла
    configBytes, err := ioutil.ReadFile(configPath)  
    if err != nil {
    	  return errors.Wrap(err, failed to read config file)
    }
  	//десериализация
    if err = yaml.Unmarshal(configBytes, config); err != nil {  
     	 return errors.Wrap(err, failed to unmarshal yaml config)
    }
    return nil
}

Выше в разделе "Работа с REST-запросами" был пример передачи конфигурационного объекта APIConfigResult в NewClient

Работа с dependency injection

Про dependency injection есть множество статей, вот тут подробно описано, что это такое, зачем он нужен, и как с ним взаимодействовать в Go.

В своем проекте использую dependency injection, потому что зависимостей становилось все больше и больше. A c помощью di удалось облегчить читаемость, избежать циклических зависимостей, упростить инициализацию. В тестах это особенно актуально, чтоб не заводить на каждый тест куча экземпляров объектов.

Для dependency injection использую библиотеку "go.uber.org/dig"

Как работает фреймворк

Фреймворк DI строит граф зависимостей на основе «поставщиков», о которых вы ему сообщаете, а затем определяет способ создания ваших объектов.

c := common.Before(t)

В ней происходит создание контейнера для инициализации компонент для dependency injection.

func Before(t *testing.T) *di.Components {
    var c *di.Components
    allure.BeforeTest(t,
                      allure.Description("Init Components"),
                      allure.Action(func() {
                        	SetGomegaParameters(t)
                        	var err error
                        	//создание контейнера
                        	c, err = di.BuildContainer()
                        	gomega.Expect(err).Should(gomega.Not(gomega.HaveOccurred()),
                                                  fmt.Sprintf("unable to build container: %v", dig.RootCause(err)))
                      }))
		return c // возвращает все компоненты
}

В функции buildContainer

//структура с нужными нам компонентами
type Components struct {
    DBProcessing        *processing.Client
    UserService         *user.Client
    Logger log.Service
    Config config.APIConfigResult
}

func BuildContainer() (*Components, error) {
    c := dig.New() //создание контейнера
    servicesConstructors := []interface{}{  
      //передаем конструкторы нужных нам сервисов
      config.NewAPIConfig,
      log.NewLoggerService,
      processing.NewClient,
      user.NewClient,
    }
//продолжение ниже
...

В dig есть две функции provide и invoke. Первая используется для добавления поставщиков, вторая — для извлечения полностью готовых объектов из контейнера.

Список конструкторов для разных типов добавляется в контейнер с помощью метода Provide, то есть мы поставляем все зависимости в контейнер.

...
		//продолжение 
    for _, service := range servicesConstructors {
      	//поставка конструкторов сервисов в di
        err := c.Provide(service)
        if err != nil {
          return nil, err
        }
    }

    comps, compsErr := initComponents(c) //инициализация
    if compsErr != nil {
        return nil, compsErr
    }
    return comps, nil
}

В методе initComponents вызывается функция Invoke. Dig вызывает функцию с запрошенным типом, создавая экземпляры только тех типов, которые были запрошены функцией. Если какой-либо тип или его зависимости недоступны в контейнере, вызов завершается ошибкой.

func initComponents(c *dig.Container) (*Components, error) {
    var err error
    t := Components{}
    err = c.Invoke(func( // вызов с запрашиваемыми типами
        processingClient *processing.Client, 
        userService *user.Client,
        logger log.Service,
        conf config.APIConfigResult,
    ) {
        t.DBProcessing = processingClient
        t.Config = conf
        t.Logger = logger
        t.UserService = userService
    })
    if err != nil {
      	return nil, err
    }

    return &amp;t, nil
}

После инициализации возвращаем наши проинициализированные компоненты.

Теперь можно использовать все созданные экземпляры объектов в тестах, обращаясь так: с.Config, с.Logger и т.д.

Генерация отчета локально и в Gitlab-CI

Allure-report локально

Чтобы сгенерировать Allure отчет локально, нужно:

  1. Скачать Allure

    brew install allure //on mac os
  2. Установить ALLURE_RESULTS_PATH в environments параметр. В этой папке будут храниться результаты после прогона тестов

    ALLURE_RESULTS_PATH=/Users/user/IdeaProjects/integration-tests/tmp/
  3. Запустить тесты с указанными environment переменными

  4. Далее необходимо сгенерировать отчет на основе результатов прогона. Для этого запускаем команду allure generate, указывая в параметре путь к папке с результатами(документация).

    allure generate /Users/user/IdeaProjects/integration-tests/tmp/allure-results --clean

И в папке allure-report теперь можно увидеть index.html с отчетом.

Gitlab-сi

Тесты запускаются в докере напротив стенда, где уже запущены все сервисы, и после прохождения результаты выгружаются на allure-сервер. Запускаются по scheduler в gitlab-ci.

Runner в Scheduler
Runner в Scheduler
Пример job-ы в .gitlab-ci.yml
Пример job-ы в .gitlab-ci.yml

В gitlab-ci.yml  в job в before-script для импорта результатов скачиваем allurectl из github

- wget <https://github.com/allure-framework/allurectl/releases/download/1.16.5/allurectl_linux_386> -O /usr/bin/allurectl

Создаем папку, куда будут выгружаться результаты.

- mkdir .allure
- mkdir -p /usr/bin/allure-results

И создаем launch, в который будут записываться результаты.

- allurectl launch create --format "ID" --no-header > .allure/launch
- export ALLURE_LAUNCH_ID=$(cat .allure/launch)

При запуске тестов в докере указываем  ALLURE_LAUNCH_ID.

- docker run --name tests -e ENV=stable -e ALLURE_LAUNCH_ID -e $IMAGE_NAME:dev || true true

Копируем результаты из докера

- docker cp tests:/allure-results/. /usr/bin/allure-results
- ls /usr/bin/allure-results

Выгружаем с помощью команды allurectl

allurectl upload /usr/bin/allure-results 

Для запуска в гитлабе нужно прописать variables для работы с Allure

Для всех ci-систем примеры можно найти тут.

Дока по импорту результатов в гитлабе.

Пример от создателей allure в гитлабе gitlab-ci.yml


В заключение

Хочется сказать, что это не призыв делать именно так, а лишь один из множества возможных способов автоматизации тестов.

Надеюсь что-то из этого было для вас полезно и не слишком скучно, старалась уместить все самое главное. И, надеюсь, что после статьи к вам пришли озарение и новые идеи для автотестов.

Если есть какие-то вопросы, или о чем-то более подробно стоит написать, то жду комментариев =)