В этой статье расскажу:
как писала интеграционные тесты на Go
с какими проблемами столкнулась
с какими библиотеками и инструментами работаю
Эта статья для тех:
кто впервые столкнулся с Go, как когда-то я
кому интересно, как можно взаимодействовать с Go в тестировании
кто не знает, с чего начать
О чем будем говорить:
Выбор языка Go и Allure
Почему выбрана Allure-go библиотека
Как выглядит Allure-go
Assertion-ы в go, выбор библиотеки
O gomega
Как интегрировать gomega с allure
Про то, как устроены тесты
Работа с REST-запросами
Работа с базой данных
Работа с конфигом
Работа с dependency injection
Генерация отчета локально и в Gitlab-CI
По терминам
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
func 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+“)
func (Steps s) SendTransaction(String 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-а в тестах, это неявные (умные) ожидания — когда ожидаешь какое-то событие в течение промежутка времени.
Критерии поиска библиотеки:
Совместимость с Go, Allure
Наличие документации
Умные ожидания
Возможность проверять несколько полей в объектах, сами объекты с разными условиями
Вывод всего объекта, если есть ошибка в одном из полей (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, то стоит их тоже рассмотреть. Выбирайте библиотеки исходя из своих собственных задач.
Статьи со сравнением тестовых фреймворков
https://bmuschko.com/blog/go-testing-frameworks/
https://knapsackpro.com/testing_frameworks/alternatives_to/testing
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, &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 &t, nil
}
После инициализации возвращаем наши проинициализированные компоненты.
Теперь можно использовать все созданные экземпляры объектов в тестах, обращаясь так: с.Config
, с.Logger
и т.д.
Генерация отчета локально и в Gitlab-CI
Allure-report локально
Чтобы сгенерировать Allure отчет локально, нужно:
Скачать Allure
brew install allure //on mac os
Установить ALLURE_RESULTS_PATH в environments параметр. В этой папке будут храниться результаты после прогона тестов
ALLURE_RESULTS_PATH=/Users/user/IdeaProjects/integration-tests/tmp/
Запустить тесты с указанными environment переменными
Далее необходимо сгенерировать отчет на основе результатов прогона. Для этого запускаем команду allure generate, указывая в параметре путь к папке с результатами(документация).
allure generate /Users/user/IdeaProjects/integration-tests/tmp/allure-results --clean
И в папке allure-report теперь можно увидеть index.html с отчетом.
Gitlab-сi
Тесты запускаются в докере напротив стенда, где уже запущены все сервисы, и после прохождения результаты выгружаются на allure-сервер. Запускаются по scheduler в gitlab-ci.
В 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
В заключение
Хочется сказать, что это не призыв делать именно так, а лишь один из множества возможных способов автоматизации тестов.
Надеюсь что-то из этого было для вас полезно и не слишком скучно, старалась уместить все самое главное. И, надеюсь, что после статьи к вам пришли озарение и новые идеи для автотестов.
Если есть какие-то вопросы, или о чем-то более подробно стоит написать, то жду комментариев =)
Привет всем. Меня зовут Таня. Я автоматизирую на Go уже около года в компании Vivid Money. До этого занималась 4 года автоматизацией тестов на Java.