Содержание урока · 10 разделов
Каркас сьюта из предыдущего урока — структура со встраиванием suite.Suite и методами TestXxx — пока живёт без расписания: кто создаёт поля, кто чистит и в каком порядке это происходит, неясно. Testify закрывает вопрос набором хуков — методов с фиксированными именами, которые библиотека вызывает сама: SetupSuite и TearDownSuite вокруг всего прогона, SetupTest и TearDownTest вокруг каждого метода, SetupSubTest и TearDownSubTest вокруг каждого s.Run. Механика знакомая: рефлексия собирает методы-тесты, а наличие хуков проверяется type assertion'ом к маленьким интерфейсам вроде SetupTestSuite — не объявили метод, интерфейс не реализован, testify молча пропускает. Никаких регистраций и конфигов: объявили метод с правильным именем и получателем-сьютом — он работает. Дальше — таблица вызовов, порядок на конкретном скелете, ловушка с мутирующими полями и честное сравнение с TestMain.
Каждый хук — необязательный метод с получателем-сьютом и пустой сигнатурой: func (s *StoreSuite) SetupTest(). Имена фиксированы: SetupSuite, SetupTest, SetupSubTest, TearDownSubTest, TearDownTest, TearDownSuite. Под капотом suite.Run проверяет, реализует ли сьют интерфейсы SetupAllSuite, SetupTestSuite, TearDownTestSuite и прочие — по одному на хук, — и вызывает методы только при совпадении. Отсюда два следствия. Опечатка в имени превращает хук в обычный метод: пакет компилируется, тесты зеленеют, а подготовки нет — самое тихое место сьютов, проверяйте имена по подсказке редактора. И рядом с основной шестёркой живут два соседних хука с аргументами: BeforeTest(suiteName, testName) и AfterTest(suiteName, testName) — их зовут вокруг каждого метода, когда нужны имена; BeforeTest срабатывает сразу после SetupTest. В типичном сьюте хватает шестёрки, а пара с именами выручает в журналах и общей телеметрии прогона.
Хук → сколько раз → для чего#
| Хук | Вызовов за прогон | Типичное содержимое |
|---|---|---|
| SetupSuite | 1 на сьют | дорогое и общее: соединение с БД, контейнер, тестовый сервер |
| SetupTest | 1 на каждый метод | чистое состояние: записи, файлы, сброс полей сьюта |
| SetupSubTest | 1 на каждый s.Run | состояние подтеста: свежий буфер, чистый каталог |
| TearDownSubTest | 1 на каждый s.Run | уборка подтеста, в том числе провалившегося |
| TearDownTest | 1 на каждый метод | уборка после метода; выполняется даже при провале теста |
| TearDownSuite | 1 на сьют | закрыть соединение, погасить контейнер, удалить каталог |
Скелет с хуками и маркерами порядка
GOtype StoreSuite struct {
suite.Suite
}
func (s *StoreSuite) SetupSuite() { fmt.Println("setup SUITE") }
func (s *StoreSuite) SetupTest() { fmt.Println("setup TEST") }
func (s *StoreSuite) SetupSubTest() { fmt.Println(" setup SUB") }
func (s *StoreSuite) TearDownSubTest() { fmt.Println(" teardown SUB") }
func (s *StoreSuite) TearDownTest() { fmt.Println("teardown TEST") }
func (s *StoreSuite) TearDownSuite() { fmt.Println("teardown SUITE") }
func (s *StoreSuite) TestOne() {
s.Run("первый", func() { /* ... */ })
s.Run("второй", func() { /* ... */ })
}
func (s *StoreSuite) TestTwo() { /* ... */ }Маркеры Println делают расписание видимым: setup SUITE один раз, затем setup TEST перед TestOne, внутри — пара setup/teardown SUB на каждый s.Run, teardown TEST после метода — и всё повторяется для TestTwo. Прогнать скелет можно локальным go test -v: консоль покажет ровно эту последовательность.
Порядок вызовов: от SetupSuite до TearDownSuite#
Для сьюта с двумя методами маршрут такой: SetupSuite → SetupTest → TestOne → TearDownTest → SetupTest → TestTwo → TearDownTest → TearDownSuite. Внутри метода с подтестами между SetupTest и TearDownTest укладываются пары SetupSubTest → тело → TearDownSubTest на каждый s.Run. Два свойства запомните сразу. TearDownTest гарантированно выполняется и при провале метода: testify зовёт уборку в defer, так что require-остановка или паника чистку не отменяют — то же верно и для TearDownSuite. И методы идут в алфавитном порядке: рефлексия перебирает методы по имени, а не по строкам файла, поэтому TestAdd выполнится раньше TestCreate, как бы вы ни расставили объявления. На отдельный прогон это не влияет, но вместе с мутациями полей даёт тесты, которые зелёные только в одном порядке запуска — об этом ниже.
Проверка: разовые хуки
Сьют с пятью методами прогоняется целиком. Какие хуки выполняются ровно один раз?
SetupSuite: дорогое и общее#
SetupSuite — место для всего, что дорого создавать и что переживёт весь прогон: соединение с базой, миграции схемы, запуск тестового контейнера, поднятый HTTP-сервер. Результат складывается в поля сьюта, и методы видят его готовым. Граница у щедрости та же: SetupSuite выполняется один раз, и всё, что он положил в поля, делится всеми методами. Соединение делить можно и нужно, а записи в таблице — нельзя: первый же метод, создавший строку, испортит подсчёты остальным. Рабочее деление: SetupSuite — ресурсы, SetupTest — данные. Если подключение в SetupSuite не удалось, проверяйте ошибку через s.Require().NoError: провал подготовки должен красить прогон сразу, а не превращаться в панику на nil-соединении в первом методе. Тесты без базы отчитались бы десятком ложных провалов вместо одного честного.
Ресурсы в SetupSuite, данные в SetupTest
GOtype UserSuite struct {
suite.Suite
db *sql.DB
users *UserRepo
}
func (s *UserSuite) SetupSuite() {
db, err := sql.Open("pgx", testDSN)
s.Require().NoError(err)
s.Require().NoError(db.Ping())
s.db = db
s.users = NewUserRepo(db)
}
func (s *UserSuite) TearDownSuite() {
s.Require().NoError(s.db.Close())
}
func (s *UserSuite) SetupTest() {
s.Require().NoError(SeedUsers(s.db, "anna", "boris"))
}
func (s *UserSuite) TearDownTest() {
s.Require().NoError(TruncateUsers(s.db))
}Соединение открывается один раз и закрывается в парном хуке; данные заливаются и сносятся на каждом методе. Проверки через s.Require() — прямое продолжение набора проверок ошибок из урока про Error и NoError: в подготовке нужен именно require, продолжать метод без базы бессмысленно.
Сьют-антипаттерн: TestCreate пишет в поле s.lastID, TestRead молча на него полагается — в полном прогоне оба зелёные. Спустя месяц кто-то запускает go test -run TestUserSuite/TestRead и получает падение: поле нулевое, потому что TestCreate не выполнялся, а алфавитный порядок методов ничего не гарантирует. Сьют — не сценарий с шагами, а контейнер независимых тестов: каждый метод обязан получать своё состояние из SetupTest, а не из результатов соседа. Перестанет тест зависеть от соседа — перестанет падать от -run, переименований и перестановок.
SetupTest и TearDownTest: чистое состояние#
SetupTest — самый занятой хук: перед каждым методом он восстанавливает мир. Сюда идёт пересоздание записей, чистые каталоги (s.T().TempDir() даёт путь, уникальный на тест), сброс карт и счётчиков в полях, пересоздание клиентов поверх общего соединения. Обратная сторона частоты: тяжёлые глобальные ресурсы сюда тащить не стоит — пересоздавать соединение на каждый метод значит платить за каждое открытие; им место в SetupSuite. TearDownTest — зеркальная уборка, и она честная: testify держит её в defer, так что даже require-остановка или паника внутри метода уборку не отменяет. Соль пары — изоляция: два метода не должны знать друг о друге, и любой обязан проходить в одиночку под -run. Проверка изоляции дешёвая: прогоните сьют дважды — целиком и с фильтром по одному методу. Расхождение сразу показывает метода-жильца, который живёт на чужой подготовке.
Подтесты с SetupSubTest и TearDownSubTest
GOfunc (s *LogSuite) SetupTest() {
s.dir = s.T().TempDir() // каталог на весь метод
}
func (s *LogSuite) SetupSubTest() {
s.lines = nil // чистый буфер на каждый подтест
}
func (s *LogSuite) TearDownSubTest() {
s.flush() // сброс буфера после каждого s.Run
}
func (s *LogSuite) TestFlush() {
s.Run("две строки", func() {
s.lines = append(s.lines, "a", "b")
s.flush()
s.Len(s.read(), 2)
})
s.Run("пустой буфер", func() {
s.flush()
s.Empty(s.read())
})
}SetupTest один раз даёт методу каталог, а SetupSubTest и TearDownSubTest обнуляют буфер вокруг каждого s.Run — между двумя подтестами проходят teardown первого и setup второго. Полный прогон с -v покажет вложенность TestLogSuite/TestFlush/две строки: тестовые пакеты исполняются только локальным go test, песочница в браузере их не берёт.
Проверка: что между подтестами
Метод TestFlush зовёт два s.Run подряд. Что выполняется между первым и вторым подтестом?
TestMain против SetupSuite#
TestMain(m *testing.M) — соседний механизм с другой зоной действия: одна функция на весь пакет, а не на сьют. Точки входа go test и её жизнь в пакете разбирались ещё в уроке про первый assert: TestMain управляет запуском целиком — подготовка до m.Run(), сбор кода возврата и os.Exit после. Каркас короткий: подготовка, code := m.Run(), уборка, os.Exit(code) — всё, что объявлено до и после m.Run(), охватывает весь пакет. Тестовому контейнеру на пакет или глобальному реестру — место здесь. SetupSuite скромнее: скоуп — один сьют; в пакете их может быть несколько рядом с плоскими функциями, и хуки каждого видят только свой мир. Отсюда правило: нужда одного сьюта — хуки, нужда всего пакета — TestMain. Ошибка на обоих уровнях одинаковая — перепутать масштаб: TestMain с подготовкой под конкретный сьют заставит плоские тесты платить за чужую базу, а TestMain, забывший m.Run(), молча не запустит ни одного теста.
TestMain в пакете один, и он распоряжается выходом процесса: привычный каркас — code := m.Run(); os.Exit(code). Забытый m.Run() означает, что тесты не выполнятся вовсе, а завершение останется успешным: зелёный отчёт при нулевой работе. Вторая ловушка — TestMain видит все тесты пакета, включая плоские функции и чужие сьюты: глобальная подготовка в нём навязывается всем. Если ресурс нужен одному сьюту, ему место в SetupSuite; TestMain оставьте тому, что делится всем пакетом тестов.
Полный порядок вызовов на скелете из урока#
- SetupSuite — один раз перед всеми методами.
- SetupTest — перед первым методом TestOne.
- Тело TestOne; вокруг каждого s.Run — SetupSubTest и TearDownSubTest.
- TearDownTest — после первого метода, даже если он провалился.
- SetupTest снова — перед TestTwo, мир пересоздан.
- Тело TestTwo, затем TearDownTest.
- TearDownSuite — один раз после всех методов.
Соберите хуки по местам
Допишите имена хуков: разовая подготовка, чистое состояние на метод, уборка после подтеста.
Хуки в порядке выполнения
Расставьте объявления хуков в том порядке, в каком testify их вызовет для одного метода с одним s.Run.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Сколько раз сработают хуки?
Прочитайте код и выберите, что он выведет:
var setups, subs int
func (s *MathSuite) SetupTest() { setups++ }
func (s *MathSuite) SetupSubTest() { subs++ }
func (s *MathSuite) TestSum() {
s.Run("единицы", func() { s.Equal(2, 1+1) })
s.Run("десятки", func() { s.Equal(20, 10+10) })
}
func (s *MathSuite) TestMul() {
s.Equal(6, 2*3)
}Карточки: хуки сьюта
Что дальше#
Шесть хуков, порядок вызовов, деление «ресурсы в SetupSuite, данные в SetupTest», мутирующие поля и границы TestMain — с этим набором сьют перестаёт быть коробкой с полями-свалками и становится расписанием. Следующая ступень курса — табличные тесты и моки: сьют с хуками — готовый контейнер для того и другого, потому что подготовка уже вынесена из методов. Если базовая механика — методы без t, точка входа, s.Run — ещё тормозит, вернитесь к основам сьютов: порядок хуков читается только поверх каркаса. Общий маршрут и соседние темы — на странице testify-курса: после сьютов идут табличные тесты, где хуки соединяются со слайсом кейсов.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Шесть хуков: SetupSuite и TearDownSuite — один раз, SetupTest и TearDownTest — на каждый метод, SetupSubTest и TearDownSubTest — на каждый s.Run.
- Хуки — необязательные методы с фиксированными именами; testify находит их type-ассертами к интерфейсам вида SetupTestSuite; опечатка превращает хук в обычный метод.
- Порядок: SetupSuite → SetupTest → метод → TearDownTest → … → TearDownSuite; методы идут в алфавитном порядке, а не по объявлению.
- TearDownTest и TearDownSuite выполняются даже при провале теста — уборка живёт в defer.
- Деление: ресурсы (соединение, контейнер) — в SetupSuite, данные (записи, файлы) — в SetupTest.
- Поля сьюта мутабельны: метод, читающий чужой результат, ломается под -run — чистое состояние восстанавливает SetupTest.
- TestMain — один на пакет и управляет os.Exit(m.Run()); забытый m.Run() даёт зелёный отчёт без тестов.
Домашнее задание
Добавьте в сьют из прошлого урока все шесть хуков с маркерами fmt.Println и прогоните go test -v: сверьте порядок с таблицей. Затем перенесите подготовку из методов в SetupTest, дорогое соединение — в SetupSuite и убедитесь, что каждый метод проходит в одиночку под go test -run по своему имени.
Личные заметкиПусто
Вопросы и ответы по уроку
SetupSuite упал — что произойдёт с тестами?
Прогон прерывается: testify перехватит провал подготовки и покрасит весь тест сьюта, не запустив ни одного метода. Методы выполняются только после успешного SetupSuite — поведение правильное: тесты без базы отчитались бы десятками ложных провалов вместо одного честного.
Можно ли готовить всё в SetupSuite и не писать SetupTest?
Формально да, но состояние перестанет быть чистым: первый метод, изменивший записи или поле, испортит прогон остальным, и тесты начнут зависеть от алфавитного порядка. Дорогое и общее — соединение — оставьте в SetupSuite, а данные восстанавливайте SetupTest перед каждым методом.
Чем SetupSubTest отличается от SetupTest на практике?
Частотой и скоупом: SetupTest выполняется один раз на метод до первого s.Run, SetupSubTest — перед каждым подтестом. Каталог, общий клиент и соединение — на метод; свежий буфер, чистый сегмент состояния или отдельная запись — на подтест внутри s.Run.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Табличные тесты с testify
- Знакомство с mock.Mock: первый мок
- On и Return: настраиваем поведение заглушек
- Аргументы вызова: Anything и свои matcher'ы
- Test suite: suite.Suite и его методы
- msgAndArgs: свои сообщения в проверках
- Проверка паник: Panics и PanicsWithValue
- True, False, Nil, NotNil и Empty: простые проверки
- Все курсы по Go
Следующая ступень учебного пути
Viper: конфигурация приложений на Go
Интерактивный курс Viper для Go: YAML и env-переменные, приоритеты источников, Unmarshal, WatchConfig, профили окружений и секреты. 25 уроков с примерами.
- Собирать конфиг из файлов, окружения и флагов в один реестр
- Читать значения типизированно: Get, GetString, Unmarshal в структуры
- Задавать приоритеты источников: флаг > env > файл > default
- Перечитывать конфиг на лету через WatchConfig и OnConfigChange