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

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

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

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

Хуки сьюта: SetupSuite, TearDownTest и компания

Хуки сьюта testify — SetupSuite, SetupTest и пары с TearDown — превращают сьют из коробки с полями в расписание: раз на прогон, на каждый метод, на каждый подтест. Перепутанные хуки дают соединение, открытое десятью способами, и состояние, которое течёт между тестами. Здесь разберём все шесть хуков, порядок вызовов, что готовить в каком и чем TestMain отличается от SetupSuite.

Содержание урока · 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. В типичном сьюте хватает шестёрки, а пара с именами выручает в журналах и общей телеметрии прогона.

Хук → сколько раз → для чего#

ХукВызовов за прогонТипичное содержимое
SetupSuite1 на сьютдорогое и общее: соединение с БД, контейнер, тестовый сервер
SetupTest1 на каждый методчистое состояние: записи, файлы, сброс полей сьюта
SetupSubTest1 на каждый s.Runсостояние подтеста: свежий буфер, чистый каталог
TearDownSubTest1 на каждый s.Runуборка подтеста, в том числе провалившегося
TearDownTest1 на каждый методуборка после метода; выполняется даже при провале теста
TearDownSuite1 на сьютзакрыть соединение, погасить контейнер, удалить каталог

Скелет с хуками и маркерами порядка

GO
type 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

GO
type 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, продолжать метод без базы бессмысленно.

SetupTest и TearDownTest: чистое состояние#

SetupTest — самый занятой хук: перед каждым методом он восстанавливает мир. Сюда идёт пересоздание записей, чистые каталоги (s.T().TempDir() даёт путь, уникальный на тест), сброс карт и счётчиков в полях, пересоздание клиентов поверх общего соединения. Обратная сторона частоты: тяжёлые глобальные ресурсы сюда тащить не стоит — пересоздавать соединение на каждый метод значит платить за каждое открытие; им место в SetupSuite. TearDownTest — зеркальная уборка, и она честная: testify держит её в defer, так что даже require-остановка или паника внутри метода уборку не отменяет. Соль пары — изоляция: два метода не должны знать друг о друге, и любой обязан проходить в одиночку под -run. Проверка изоляции дешёвая: прогоните сьют дважды — целиком и с фильтром по одному методу. Расхождение сразу показывает метода-жильца, который живёт на чужой подготовке.

Подтесты с SetupSubTest и TearDownSubTest

GO
func (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(), молча не запустит ни одного теста.

Полный порядок вызовов на скелете из урока#

  1. SetupSuite — один раз перед всеми методами.
  2. SetupTest — перед первым методом TestOne.
  3. Тело TestOne; вокруг каждого s.Run — SetupSubTest и TearDownSubTest.
  4. TearDownTest — после первого метода, даже если он провалился.
  5. SetupTest снова — перед TestTwo, мир пересоздан.
  6. Тело TestTwo, затем TearDownTest.
  7. TearDownSuite — один раз после всех методов.

Соберите хуки по местам

Допишите имена хуков: разовая подготовка, чистое состояние на метод, уборка после подтеста.

func (s *OrderSuite) () { s.db = openTestDB() // один раз на весь сьют } func (s *OrderSuite) () { s.cart = NewCart() // перед каждым методом } func (s *OrderSuite) () { os.RemoveAll(s.tmp) // после каждого s.Run } func (s *OrderSuite) TestTotal() { s.Run("со скидкой", func() { s.Equal(90, s.cart.Total(100)) }) }

Хуки в порядке выполнения

Расставьте объявления хуков в том порядке, в каком 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 / TearDownSuite

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

Один раз на прогон сьюта: дорогое и общее — соединение с БД, контейнер, сервер. Всё, что положили в поля, делится всеми методами.

1 / 6

Что дальше#

Шесть хуков, порядок вызовов, деление «ресурсы в 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 по своему имени.

Личные заметкиПусто
Хранятся только в вашем браузере (Testify · Хуки сьюта: SetupSuite, TearDownTest и компания)

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

SetupSuite упал — что произойдёт с тестами?

Прогон прерывается: testify перехватит провал подготовки и покрасит весь тест сьюта, не запустив ни одного метода. Методы выполняются только после успешного SetupSuite — поведение правильное: тесты без базы отчитались бы десятками ложных провалов вместо одного честного.

Можно ли готовить всё в SetupSuite и не писать SetupTest?

Формально да, но состояние перестанет быть чистым: первый метод, изменивший записи или поле, испортит прогон остальным, и тесты начнут зависеть от алфавитного порядка. Дорогое и общее — соединение — оставьте в SetupSuite, а данные восстанавливайте SetupTest перед каждым методом.

Чем SetupSubTest отличается от SetupTest на практике?

Частотой и скоупом: SetupTest выполняется один раз на метод до первого s.Run, SetupSubTest — перед каждым подтестом. Каталог, общий клиент и соединение — на метод; свежий буфер, чистый сегмент состояния или отдельная запись — на подтест внутри s.Run.

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

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

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

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

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

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

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