Привет, Хабр! Вы можете помнить меня по предыдущей статье про Allure-Go, в которой мы коснулись самой макушечки нашей скромной наработки. Сегодня же мы накидаем пару тестов с нуля, разберём подробно примеры и посмотрим, чего же нам удалось в итоге добиться.
Много коммитов утекло с того момента, когда мы с вами общались в прошлый раз. Вышло обновление 0.5, которое привнесло множество изменений, в том числе и в интерфейсах, а также обновление 0.6, которое добавило поддержку test plan из TestOps. Более подробно об обновлениях написано в Release Notes.
С чего начать?
Первым делом нужно установить зависимости.
go get github.com/ozontech/allure-go/pkg/allure
go get github.com/ozontech/allure-go/pkg/framework
Теперь мы должны прикинуть, в каком конкретно виде нам понадобятся тесты. В Allure-Go существует несколько вариантов хранения и запусков тестов:
Runner из пакета framework/runner, он позволяет запускать единичные тесты. Подробнее можно почитать в Readme,
suite — набор тестов, позволяет объединить тесты по бизнес-логике или по общему фактору.
Сегодня мы будем говорить о suite (иначе, ей-богу, никакой статьи нам не хватит), однако принципы, изложенные в этой статье, одинаково справедливы как для тестов в раннере, так и для самостоятельных тестов.
Концепция структур в виде тест-комплектов позаимствована из фреймворка testify.
Тестовый suite — это аналог тест-класса из JUnit/TestNG. Идея проста: мы имеем объект, методами которого являются тесты. Во время запуска тестов мы передаём экземпляр структуры в исполняющий метод — и он в свою очередь запускает методы структуры как обычные тесты.
Напишем самый простой тест
Ух, звучит, конечно, не так просто, как идея, но на практике всё намного проще.
Вот простой пример:
package test
import (
"testing"
"github.com/ozontech/allure-go/pkg/framework/provider"
"github.com/ozontech/allure-go/pkg/framework/suite"
)
type MyFirstSuite struct {
suite.Suite
}
func (s *MyFirstSuite) TestMyFirstTest(t provider.T) {
}
func TestSuiteRunner(t *testing.T) {
suite.RunSuite(t, new(MyFirstSuite))
}
Давайте разберёмся, в чём же соль.
type MyFirstSuite struct
— структура, которой суждено хранить в себе наши тесты. Чтобы структуру можно было использовать как тест-сьют, необходимо расширить её с помощью структуры suite.Suite.
func (s *MyFirstSuite) TestMyFirstTest(t provider.T)
— наш тест-болванка.
Важно! Обратите внимание, что тест принимает в качестве аргумента интерфейс provider.T. Это наш основной инструмент работы с тестами, мы вернёмся к нему чуть позже.
func TestSuiteRunner(t *testing.T)
— функция запуска тестов. Поскольку Allure-Go является обёрткой над библиотекой testing, нам требуется получить тестовый контекст *testing.T. Для нас эта функция является отправной точкой в запуске тестов.
suite.RunSuite(t, new(MyFirstSuite)
) — метод, запускающий сьюты.
Note: по умолчанию в Allure-отчёте в качестве имени сьюта будет использоваться имя структуры. Однако можно запустить тест-комплект с помощью метода suite.RunNamedSuite и передать то имя сьюта, которое больше нравится.
Вы можете справедливо заметить: «Друже, ты говорил, что эта штука похожа на JUnit/TestNG. А как же тут с before/after-хуками?» На что я не менее справедливо отвечу: «Всё тут с ними отлично». Давайте расширим наш пример:
package test
import (
"testing"
"github.com/ozontech/allure-go/pkg/framework/provider"
"github.com/ozontech/allure-go/pkg/framework/suite"
)
// Структура сьюта
type MyFirstSuite struct {
suite.Suite
}
// Сработает один раз перед запуском сьюта
func (s *MyFirstSuite) BeforeAll(t provider.T) {
}
// Сработает один раз после того, как все тесты завершатся
func (s *MyFirstSuite) AfterAll(t provider.T) {
}
// Будет срабатывать каждый раз перед началом теста
func (s *MyFirstSuite) BeforeEach(t provider.T) {
}
// Будет срабатывать каждый раз после окончания теста
func (s *MyFirstSuite) AfterEach(t provider.T) {
}
func (s *MyFirstSuite) TestMyFirstTest(t provider.T) {
}
func TestSuiteRunner(t *testing.T) {
suite.RunSuite(t, new(MyFirstSuite))
}
Выглядит знакомо, не так ли?
Однако есть некоторые неочевидные особенности нашей имплементации BeforeEach
:
Если вы инициализируете некоторые данные в структуре сьюта в методе
BeforeEach
и ваши тесты бегают параллельно, то вы, скорее всего, столкнётесь с race condition. Этого можно избежать, например, с помощью инструментов синхронизации или мьютексов либо инициализировав все нужные данные в BeforeAll.В
BeforeEach
можно проставлять общие для всех тестов теги и лейблы для Allure. Например:
func (s *MyFirstSuite) BeforeEach(t provider.T) {
t.Epic("My Epic")
t.Feature("My Feature")
// и так далее
}
Тестовый контекст provider.T
Итак, с запуском сьютов разобрались. Теперь давайте погрузимся в сами тесты и разберёмся, какие возможности нам даёт provider.T
. Для начала накидаем пару ассертов:
func (s *MyFirstSuite) TestMyFirstTest(t provider.T) {
test := "test"
t.Require().NotNil(test)
t.Require().Equal(test, "test")
}
и заглянем в отчёт:
Как видно из скриншота, оба ассерта подсветились в нашем репорте.
Note: Allure-Go поддерживает паттерн Soft Assert в виде t.Assert(). Разница в том, что t.Require() сразу уронит тест, а t.Assert() позволит ему дойти до конца.
Отлично! Но хотелось бы сгруппировать проверки в общий шаг (allure.Step), не так ли? В этом нам поможет метод t.WithNewStep(string, provider.StepCtx, …allure.Parameter)
:
func (s *MyFirstSuite) TestMyFirstTest(t provider.T) {
test := "test"
t.WithNewStep("My first step", func(stepCtx provider.StepCtx) {
stepCtx.Require().NotNil(test)
stepCtx.Require().Equal(test, "test")
}, allure.NewParameter("time", time.Now()))
}
Note:
provider.StepCtx
— это интерфейс, который практически во всём похож на provider.T. Такая заморочка с разделением интерфейсов нужна, чтобы прослеживать вложенность шагов во время исполнения.
Ну и было бы супер, если бы мы сохраняли какой-то параметр, например время (а почему бы и нет?).
Глянем, что же у нас в итоге получается:
Совсем другое дело :)
Итого, provider.T позволяет:
проставлять Allure-лейблы (
Feature
,Epic
,Severity
,Tag
, etc),размечать тесты на шаги (
Step
,WithNewStep
,WithNewAsyncStep
),получать доступ к обёрнутым шагами ассертам (
Require
,Assert
),управлять поведением теста (
XSkip
,Parallel
,Fail
, etc).
Note: Полный список возможностей описан в невероятно душной доке: provider.T и provider.StepCtx.
А что же параметризация?
Вот тут на данный момент нет красивого решения даже у нас. Давайте разберём пример на основе нашего первого теста:
func (s *MyFirstSuite) TestMySecondTest(t provider.T) {
test := "test"
testData := []string{"test0", "test1", "test2"}
for idx, text := range testData {
t.Run(text, func(t provider.T) {
data := fmt.Sprintf("%s%d", test, idx)
t.WithNewStep("My First Step", func(sCtx provider.StepCtx) {
sCtx.Require().NotNil(text)
sCtx.Require().Equal(data, text)
}, allure.NewParameter("time", time.Now()))
})
}
}
Да, нужно крутить цикл — по-другому пока никак. Но мы сейчас работаем над решением этой проблемы :)
Как же это будет выглядеть в отчёте?
Да, отлично выглядит! Обратите внимание: имя родительского теста выступает в качестве лейбла Suite для параметризованных тестов, а сьют — в качестве лейбла ParentSuite.
Важно: На скриншоте видно
TestMySecondTest
в отчёте. Для отключения опции создания отчёта у конкретного теста нужно вызвать методSkipOnPrint
из структуры provider.T.Для такого теста репорт генерироваться не будет. Автоматически этого не происходит по одной важной причине: в Go есть поддержка вложенных тестов, и нам не хотелось бы терять эту функциональность.
Не забудем про XSkip
Почти всё!
Давайте теперь разберём такую штуку, как XSkip
. Итак, представьте себе ситуацию: тёплый вечерок, вы лампово потягиваете старый добрый «Портвейн 777» банановый смузи и в двадцатый раз пересматриваете My Little Pony видеолекцию по C++. Тут вам прилетает уведомление в рабочий чат:
%QAName%! У тебя тест опять отвалился, выкатиться не можем.
И вы вспоминаете, что багу по этому тесту вы завели ещё в прошлый спринт, вчера разработчик взял её в работу, а выкатить обещал только завтра. Что же делать, чтобы и ребятам помочь, и не забыть про этот тест?
На помощь придёт наш t.XSkip()
! Давайте рассмотрим пример:
func (s *MyFirstSuite) TestMySecondTest(t provider.T) {
var test string
t.Require().Equal("test", test)
}
Тест ожидаемо упал. Что же делать?
func (s *MyFirstSuite) TestXSkip(t provider.T) {
var test string
t.XSkip()
t.Require().Equal("test", test)
}
Немного магии — и ваш нестабильный тест автоматически скипается в случае падения, а к имени теста добавляется соответствующий префикс.
Note: внимательный читатель заметил, что
XSkip
очень похож на декораторxfail
из pytest. Однако, в отличие отxfail
,XSkip
пропускает тест, а не «зеленит» его.
Параллельность в тестах
И снова вы можете спросить: «Друже! А что же с асинхронностью? Ведь testify так и не победили эту проблему. А в прошлый раз, когда ты графоманил писал увлекательную статью про Allure-Go, прелестная @Tan_tan задала тебе вопрос про параллельность в тестах, и ты сказал, что проблема решена лишь частично». На что отвечу: «Мы-таки победили. Потом, кровью, эмоциональным выгоранием, слезами… но победа оказалась в наших руках. Гордо можем заявить: Allure-Go полностью поддерживает асинхронные запуски без каких-либо но, условностей и тому подобной шелухи».
Note: то, как мы обошли проблему параллельности в тестах, заслуживает отдельного и очень развёрнутого поста. Как-нибудь обязательно об этом расскажу.
Давайте рассмотрим наши чудесные тесты в сьюте и сделаем их параллельными:
func (s *MyFirstSuite) TestMyFirstTest(t provider.T) {
test := "test"
t.Parallel() // именно этот метод вызывает параллельность
t.WithNewStep("My First Step", func(sCtx provider.StepCtx) {
sCtx.Require().NotNil(test)
sCtx.Require().Equal(test, "test")
}, allure.NewParameter("time", time.Now()))
}
func (s *MyFirstSuite) TestMySecondTest(t provider.T) {
test := "test"
t.Parallel()
for idx, text := range []string{"test0", "test1", "test2"} {
t.Run(text, func(t provider.T) {
testText = text // обязательно сохраняйте при параллельном запуске локальную переменную для параметризованных тестов во избежание race condition
data := fmt.Sprintf("%s%d", test, idx)
t.Parallel()
t.WithNewStep("My First Step", func(sCtx provider.StepCtx) {
sCtx.Require().NotNil(testText)
sCtx.Require().Equal(data, testText)
}, allure.NewParameter("time", time.Now()))
})
}
}
Но и это ещё не всё. Вы также можете использовать t.WithNewAsyncStep
для запуска асинхронных шагов. Они будут выполняться параллельно с основным потоком вашего теста. Для более точного контроля за их исполнением рекомендуется использовать sync.WaitGroup
или channel
, однако, если вы про них забудете, тест всё равно не будет считаться завершённым, пока все асинхронные шаги не подойдут к концу, и дождётся окончания всех асинхронных процессов, запущенных шагами.
Давайте рассмотрим пример:
func (s *MyFirstSuite) TestMyFirstTest(t provider.T) {
test := "test"
wg := sync.WaitGroup{} // инициализируем WaitGroup для отслеживания работы асинхронного теста
t.Parallel() // именно этот метод вызывает параллельность
wg.Add(1) // добавляем единичку к дельте ожидания (если дельта равна 0, то ждать мы перестаём)
t.WithNewAsyncStep("My First Step", func(sCtx provider.StepCtx) {
defer wg.Done() // не забываем отпустить нашу единичку после завершения функции
sCtx.Assert().NotNil(test)
sCtx.Assert().Equal(test, "test")
}, allure.NewParameter("time", time.Now()))
wg.Wait() // ну и наконец ждём
}
Важно! Крайне не рекомендуется использовать
t.Require()
сWithNewAsyncStep
по причине того, чтоtesting.T.FailNow()
грубо и бесцеремонно закрывает горутину теста черезruntime.goexit
, которая в свою очередь убивает все родительские горутины. Могут потеряться данные ваших шагов. Во избежание конфуза используйтеt.Assert()
.
Выводы
На сегодня у меня, пожалуй, всё. Примеры кода из статьи вы можете найти здесь.
В дополнение хочется отметить недавний релиз библиотеки CUTE, посвящённой тестированию HTTP, в основе которой лежит Allure-Go, и статью о ней авторства @siller174.
Лучше, чем наш новый и красивый Readme, о тонкостях Allure-Go не расскажет никто.
В следующий раз обсудим запуск получившихся тестов в пайплайне, поразмышляем о том, как можно улучшить инфраструктуру тестов, и разберёмся, что для этого понадобится.
Спасибо за внимание, берегите себя, скупайте золото и оставайтесь на связи!
JekaMas
Действительно круто!
Смущает лишь provider.T, убивающий возможность использовать многие сторонние пакеты для тестирования. Мне первым на ум пришел rapid, например.
koodeex Автор
Большое спасибо за комментарий!
Хм, кейс хороший.
Проблема с rapid, например, в том, что у них там указатель передается, а не интерфейс.
А так, с testify, например, allure-go совместима как раз потому, что ребята в свои ассерты принимают интерфейс.
Постараюсь поисследовать, как можно было бы подружить allure-go с библиотеками, которые принимают только указатели.
JekaMas
Было бы здорово.
Я лично property based тесты в каждом гошном проекте использую. И без них трудно.