Go, я создал: пишем тесты на Allure-Go
Привет, Хабр! Вы можете помнить меня по предыдущей статье про 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 не расскажет никто.
В следующий раз обсудим запуск получившихся тестов в пайплайне, поразмышляем о том, как можно улучшить инфраструктуру тестов, и разберёмся, что для этого понадобится.
Спасибо за внимание, берегите себя, скупайте золото и оставайтесь на связи!