Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 11 из 25 ~18 мин +20 XP

Test suite: suite.Suite и его методы

Testify suite — это структура со встроенным suite.Suite, у которой тесты живут методами, а не плоскими функциями. Пока тестов три, разницы не видно; когда их двадцать и у всех общая подготовка, сьют экономит и строки, и нервы. Здесь соберём минимальный сьют, заглянем, что suite.Run делает рефлексией под капотом, и очертим границу, за которой сьют превращается из помощи в помеху.

Содержание урока · 10 разделов

У пакета store десять тестов, и в каждом одни и те же первые строки: поднять клиента, открыть соединение, залить демо-данные. Плоские функции TestXxx(t *testing.T) из первых уроков такого не прощают: общего состояния между функциями нет, и подготовка либо копируется десять раз, либо уползает в глобальные переменные, которые каждый тест чистит руками. Сьют — фигура из арсенала testify ровно под эту боль: одна структура, тесты становятся её методами, общий контекст — её полями. Определение выглядит непривычно, механика спрятана в рефлексию, и вокруг сьютов годами идут споры. Здесь соберём сьют целиком: определение со встраиванием, доступ к проверкам, точку входа suite.Run, подтесты s.Run — и честно поговорим о том, когда сьют скорее мешает. Порядок хуков подготовки, таблицу вызовов и TestMain оставим следующему уроку — хукам сьюта.

Test suite: структура со встраиванием suite.Suite#

Сьют — обычная структура Go, в которую встроен тип suite.Suite из пакета github.com/stretchr/testify/suite: type StoreSuite struct { suite.Suite }. Встраивание — тот же механизм, что у структур из базового курса: методы suite.Suite, включая T, Require и Assert, становятся доступны у StoreSuite как свои. Полей сьютовой структуры никто не ограничивает — сюда складывают общее состояние: соединение с базой, настроенный клиент, каталог с фикстурами. Тесты объявляются методами с префиксом Test: func (s StoreSuite) TestCreate() — без testing.T в сигнатуре, это главное внешнее отличие от плоских функций. Testify собирает такие методы рефлексией: пробегает по типу сьюта, отбирает экспортированные методы с префиксом Test, у которых нет входных параметров и возвращаемых значений, и запускает каждый через t.Run. Метод с другим префиксом или с параметром — не тест: рефлексия его либо не заметит вовсе, либо пометит как сломанную сигнатуру.

Минимальный сьют

GO
type StoreSuite struct {
        suite.Suite // встраивание из github.com/stretchr/testify/suite

        items map[string]int // общее состояние всех методов
}

func (s *StoreSuite) TestCreate() {
        s.items["apple"] = 3

        s.Len(s.items, 1)
        s.Equal(3, s.items["apple"])
}

func (s *StoreSuite) TestDelete() {
        s.items["apple"] = 3
        delete(s.items, "apple")

        s.Empty(s.items)
}

Два метода-теста без единого t: проверки зовутся прямо на сьюте — s.Len, s.Equal и s.Empty достались от встроенного поля *assert.Assertions, которое testify подключает в момент suite.Run. Поле items одно на оба метода, и кто его наполняет и чистит — тема урока про хуки. Полный прогон такого файла требует локального go test: тестовые пакеты браузерная песочница не исполняет.

Параметра t у метода нет, но он доступен: s.T() возвращает текущий *testing.T — пригодится для Deadline, TempDir и всего, что ждёт t напрямую. Проверки живут в трёх местах, и разница между ними та же, что у assert и require из урока про выбор пакета. Прямые вызовы вроде s.Equal или s.NoError — это assert: провал помечает тест, но метод продолжает работать. s.Require() возвращает require-версию: s.Require().NoError(err) при провале останавливает текущий метод через FailNow — то, что нужно в подготовке. s.Assert() — явный способ добраться до assert-контекста; он выручает, когда имя метода на сьюте чем-то занято. Нюанс из исходников: до suite.Run этих полей не существует — Require(), вызванный раньше Run, паникует с сообщением, что сьют ещё не подключён к go test. Правило прежнее: require — для подготовки, assert — для содержательных проверок.

Проверка: сигнатура метода-теста

В сьюте объявлен метод func (s *StoreSuite) TestCreate(t *testing.T). Что случится при прогоне?

Запуск: suite.Run и одна точка входа#

Плоские тесты go test находит сам: каждая функция TestXxx в пакете — отдельный тест. У сьюта точка входа одна: func TestStoreSuite(t *testing.T) { suite.Run(t, new(StoreSuite)) } — обычная тестовая функция, видимая и компилятору, и go test. suite.Run создаёт экземпляр сьюта, подключает к нему t, рефлексией собирает методы-тесты и прогоняет каждый как подтест через t.Run. Для go test весь сьют — один тест верхнего уровня; вывод -v разворачивает вложенность: сначала RUN TestStoreSuite, под ним RUN TestStoreSuite/TestCreate и так по методам. Фильтрация остаётся в силе: go test -run 'TestStoreSuite/TestDelete' запустит сьют, но выполнит только метод TestDelete — фильтр идёт по пути подтеста. У testify есть и собственный флаг -testify.m: регулярное выражение для отбора методов сьюта, аналог -run с сьютовым скоупом.

Точка входа и вывод go test -v

GO
func TestStoreSuite(t *testing.T) {
        suite.Run(t, new(StoreSuite))
}

// $ go test -v
// === RUN   TestStoreSuite
// === RUN   TestStoreSuite/TestCreate
// === RUN   TestStoreSuite/TestDelete
// --- PASS: TestStoreSuite (0.00s)
//     --- PASS: TestStoreSuite/TestCreate (0.00s)
//     --- PASS: TestStoreSuite/TestDelete (0.00s)

Точка входа одна, и только она видна go test как тест верхнего уровня. Каждый метод сьюта suite.Run прогоняет через t.Run, поэтому в -v методы выглядят подтестами с именем TestStoreSuite/TestXxx — а падение любого из них красит общий тест TestStoreSuite.

Сколько тестов видит go test?

Прочитайте код и выберите, что он выведет:

type CalcSuite struct {
        suite.Suite
}

func (s *CalcSuite) prepare() {
        // общая подготовка методов
}

func (s *CalcSuite) TestAdd() {
        s.Equal(4, 2+2)
}

func (s *CalcSuite) TestSub() {
        s.Equal(0, 2-2)
}

func TestCalcSuite(t *testing.T) {
        suite.Run(t, new(CalcSuite))
}

s.Run: подтесты внутри сьюта#

Внутри метода-теста сценарии можно дробить дальше: s.Run(name, func()) — сьютовый аналог t.Run. Замыкание не принимает параметров: текущий t подменяется внутри сьюта, а проверки вроде s.Equal продолжают работать с новым контекстом. Вокруг каждого подтеста testify вызывает пару хуков SetupSubTest и TearDownSubTest, если они объявлены, — расписание этих вызовов разбирает следующий урок. Имена вкладываются в путь: TestParserSuite/TestParse/валидная строка — и фильтр go test -run 'TestParserSuite/TestParse/валидная' доберётся до одного конкретного подтеста. Паника внутри подтеста перехватывается: testify превращает её в провал этого подтеста с трассировкой, остальные подтесты и методы доживают до конца, а после s.Run — suite возвращает прежний t, так что состояние контекста не течёт между подтестами.

Подтесты s.Run в одном методе

GO
func (s *ParserSuite) TestParse() {
        s.Run("валидная строка", func() {
                pairs, err := Parse("a=1&b=2")
                s.Require().NoError(err)
                s.Len(pairs, 2)
        })

        s.Run("мусор на входе", func() {
                _, err := Parse("%%")
                s.Error(err)
        })
}

Оба подтеста живут внутри одного метода TestParse, и Require().NoError останавливает только свой подтест: падение валидного сценария не отменяет сценарий с мусором. Замыкания не принимают t — testify подменяет контекст сам, а имена подтестов попадают в -v и в фильтры -run.

Проверка: require внутри подтеста

В подтесте s.Run("скидка", ...) проверка s.Require().NotNil(total) провалилась. Что произойдёт?

Когда сьют вместо плоских функций#

Сьют окупается, когда методам нужен один и тот же дорогой контекст: соединение с базой, запущенный тестовый сервер, каталог с распакованными фикстурами. Такое создаётся один раз, живёт полями структуры, и каждый метод получает готовый мир вместо десяти строк подготовки. Второй случай — группа тестов одного модуля: пакет store с набором операций удобнее читать одним файлом-сьютом, где методы сгруппированы и оформлены одинаково. Третий — постепенный переход: глобальные переменные с ручной очисткой переносятся в поля с хуками, и тесты перестают зависеть от того, в каком порядке их запустили. Не нужен сьют чистым функциям: там табличный кейс в слайсе короче любого метода, а общего контекста нет вовсе. И не спасает сьют от бессистемности: структура без хуков с полями-свалками — те же глобальные переменные, только с методами.

Рабочее правило выбора#

  • Подготовка копируется три и больше раз — выносите её в поля сьюта и хуки.
  • Контекст дорогой: БД, контейнер, сервер — сьют с общим полем и SetupSuite.
  • Чистые функции и перебор кейсов — плоские тесты, сьют не добавит ясности.
  • Глобальные переменные в тестах с ручной очисткой — кандидат на поля сьюта.
  • Один модуль — один файл-сьют; соседние модули — отдельные сьюты, а не один общий.

Плоские функции и сьют рядом#

КритерийПлоские TestXxxСьют suite.Suite
Точка входакаждая функция — отдельный тестодна функция с suite.Run, методы — подтесты
Подготовкакопия в каждой функции или глобальные переменныеполя структуры и хуки
Состояние между тестамипакетные переменные, ноль гарантийполя сьюта, порядок задаёт алфавит методов
Сигнатура тестаfunc TestX(t *testing.T)func (s *Suite) TestX() без параметров
Провал проверкиassert и require с tте же проверки на s, t доступен через s.T()
Подтестыt.Runs.Run с хуками подтеста

Соберите каркас сьюта

Допишите встраивание, запуск и require-проверку в методе.

type AuthSuite struct { // встраивание пакета suite } func TestAuthSuite(t *testing.T) { suite.(t, new(AuthSuite)) } func (s *AuthSuite) TestLogin() { err := Login("anna", "s3cret") s.().NoError(err) }

Соберите метод-тест сьюта

Расставьте строки метода: сигнатура, подготовка, первый шаг, проверка, второй шаг, проверка корзины.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: основы сьютов

Термин

suite.Suite

нажмите, чтобы перевернуть
Определение

Тип из пакета github.com/stretchr/testify/suite, встраиваемый в структуру-сьют: приносит T(), Require(), Assert() и поле *assert.Assertions для прямых вызовов вида s.Equal.

1 / 6

Что дальше#

Каркас собран: структура со встраиванием, методы без t, одна точка входа, подтесты s.Run. Это половина сьютовой механики — вторая половина в хуках: SetupSuite раз на сьют, SetupTest на каждый метод, SetupSubTest на каждый подтест. Без них поля сьюты — просто красивые глобальные переменные: кто их создаёт, кто чистит и в каком порядке это происходит — тема следующего урока, там же таблица хуков и разбор TestMain против SetupSuite. Полный прогон примеров этого урока — локальный go test: сборка пакета с -v покажет ровно ту картину подтестов, что в примере выше. Если прямые вызовы s.Equal и require-вызовы s.Require().NoError ещё путаются, перечитайте правило выбора двух пакетов — на сьюты оно переносится один в один. Карта всех уроков — на странице testify-курса.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Сьют — структура со встраиванием suite.Suite из github.com/stretchr/testify/suite; тесты — методы с префиксом Test без *testing.T в сигнатуре.
  • Проверки доступны прямо на сьюте (s.Equal) — от встроенного *assert.Assertions; s.Require() даёт require-семантику, s.T() — текущий *testing.T.
  • Точка входа одна: func TestXxx(t *testing.T) { suite.Run(t, new(XxxSuite)) }; методы выполняются подтестами и раскрываются в go test -v.
  • Фильтрация сохраняется: go test -run 'TestXxx/TestMethod' выполняет отдельный метод; у testify есть и флаг -testify.m.
  • s.Run — подтест сьюта: замыкание без параметров, вокруг работают SetupSubTest и TearDownSubTest, паника перехватывается.
  • Метод с параметром не запустится — testify пометит сломанную сигнатуру; метод без префикса Test рефлексия не находит вовсе.
  • Сьют оправдан дорогим общим контекстом и группой тестов одного модуля; чистым функциям ближе плоские тесты.

Домашнее задание

Возьмите пакет с тремя плоскими тестами и одинаковой подготовкой: соберите сьют со встраиванием suite.Suite, перенесите подготовку в поле, добавьте точку входа с suite.Run. Раздробите один метод на два s.Run и прогоните go test -v: убедитесь, что методы видны подтестами и фильтр -run добирается до одного из них.

Личные заметкиПусто
Хранятся только в вашем браузере (Testify · Test suite: suite.Suite и его методы)

Вопросы и ответы по уроку

Можно ли объявить метод-тест с параметром t, как у плоской функции?

Метод скомпилируется, но testify откажется его запускать: рефлексия проверяет сигнатуру и помечает методы с параметрами и возвращаемыми значениями ошибкой invalid signature. Внутри методов t не приходит параметром — берите его через s.T(), а проверки зовите на сьюте.

Почему s.Equal есть сразу, а require только через s.Require()?

suite.Suite встраивает *assert.Assertions напрямую — assert-методы видны как свои. require-версию так же встроить нельзя: у обоих пакетов одинаковые имена методов, и встраивание обоих создало бы конфликт. Поэтому testify хранит require в закрытом поле и отдаёт его методом s.Require().

Зачем сьют, если подтесты умеет и t.Run в плоских тестах?

t.Run даёт подтесты, но не общий контекст: подготовка остаётся внутри каждой функции. Сьют добавляет поля структуры для состояния и хуки подготовки: дорогое и общее создаётся один раз, методы получают готовый мир. Для чистых функций разницы почти нет — там плоские тесты проще.

Термины урока

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

Следующая ступень учебного пути

Viper25 уроков~9 чпрактика25 уроков уже доступны

Viper: конфигурация приложений на Go

Интерактивный курс Viper для Go: YAML и env-переменные, приоритеты источников, Unmarshal, WatchConfig, профили окружений и секреты. 25 уроков с примерами.

  • Собирать конфиг из файлов, окружения и флагов в один реестр
  • Читать значения типизированно: Get, GetString, Unmarshal в структуры
  • Задавать приоритеты источников: флаг > env > файл > default
  • Перечитывать конфиг на лету через WatchConfig и OnConfigChange