---
title: "Хуки сьюта: SetupSuite, TearDownTest и компания"
description: "Хуки сьюта testify: SetupSuite и TearDownSuite один раз, SetupTest и TearDownTest на каждый тест, SetupSubTest вокруг s.Run. Порядок, TestMain и ловушки."
course: "Testify: тестирование Go с assert и require"
url: "https://golangmaster.ru/courses/testify/lessons/12-suites-hooks/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

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

*Курс «Testify: тестирование Go с assert и require», урок 12 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/testify/lessons/12-suites-hooks/*

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

Каркас сьюта из предыдущего урока — структура со встраиванием 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 на сьют | закрыть соединение, погасить контейнер, удалить каталог |

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

```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 и TearDownSuite
- SetupSuite и SetupTest
- SetupTest и TearDownTest
- Все шесть — по одному разу

SetupSuite открывает прогон, TearDownSuite закрывает; между ними пять раз повторяется пара SetupTest/TearDownTest, по кругу на каждый метод. SetupSubTest и TearDownSubTest добавляются только там, где метод зовёт s.Run.

### 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](/courses/testify/lessons/04-error-assertions/): в подготовке нужен именно 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

```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 подряд. Что выполняется между первым и вторым подтестом?

- TearDownTest, затем SetupTest
- ✅ TearDownSubTest первого подтеста, затем SetupSubTest второго
- TearDownSuite, затем SetupSuite заново
- Ничего: хуки срабатывают только вокруг методов

Пара SetupSubTest/TearDownSubTest ходит вокруг каждого s.Run: между двумя подтестами одного метода умещаются TearDownSubTest первого и SetupSubTest второго. Пара SetupTest/TearDownTest в это время не трогается — она обслуживает метод целиком.

### TestMain против SetupSuite

TestMain(m *testing.M) — соседний механизм с другой зоной действия: одна функция на весь пакет, а не на сьют. Точки входа go test и её жизнь в пакете разбирались ещё в уроке про [первый assert](/courses/testify/lessons/02-pervyj-assert/): TestMain управляет запуском целиком — подготовка до m.Run(), сбор кода возврата и os.Exit после. Каркас короткий: подготовка, code := m.Run(), уборка, os.Exit(code) — всё, что объявлено до и после m.Run(), охватывает весь пакет. Тестовому контейнеру на пакет или глобальному реестру — место здесь. SetupSuite скромнее: скоуп — один сьют; в пакете их может быть несколько рядом с плоскими функциями, и хуки каждого видят только свой мир. Отсюда правило: нужда одного сьюта — хуки, нужда всего пакета — TestMain. Ошибка на обоих уровнях одинаковая — перепутать масштаб: TestMain с подготовкой под конкретный сьют заставит плоские тесты платить за чужую базу, а TestMain, забывший m.Run(), молча не запустит ни одного теста.

> **TestMain глобальнее, чем кажется.** TestMain в пакете один, и он распоряжается выходом процесса: привычный каркас — code := m.Run(); os.Exit(code). Забытый m.Run() означает, что тесты не выполнятся вовсе, а завершение останется успешным: зелёный отчёт при нулевой работе. Вторая ловушка — TestMain видит все тесты пакета, включая плоские функции и чужие сьюты: глобальная подготовка в нём навязывается всем. Если ресурс нужен одному сьюту, ему место в SetupSuite; TestMain оставьте тому, что делится всем пакетом тестов.

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

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

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

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

```go
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))
        })
}
```

Подсказка: Раз на сьют — SetupSuite; на каждый метод — SetupTest; после каждого s.Run — TearDownSubTest.

Правильные ответы в пропусках: `SetupSuite`, `SetupTest`, `TearDownSubTest`

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

Расставьте объявления хуков в том порядке, в каком testify их вызовет для одного метода с одним s.Run.

Правильный порядок строк:

1. `func (s *StoreSuite) SetupSuite() {`
2. `func (s *StoreSuite) SetupTest() {`
3. `func (s *StoreSuite) SetupSubTest() {`
4. `func (s *StoreSuite) TearDownSubTest() {`
5. `func (s *StoreSuite) TearDownTest() {`
6. `func (s *StoreSuite) TearDownSuite() {`

### Сколько раз сработают хуки?

```go
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)
}
```

**Что выведет программа?**

- ✅ setups = 2, subs = 2
- setups = 2, subs = 3
- setups = 3, subs = 2
- setups = 1, subs = 2

SetupTest срабатывает на каждый метод-тест: TestSum и TestMul — два вызова. SetupSubTest — на каждый s.Run: два подтеста в TestSum, а TestMul подтестов не зовёт, поэтому тоже два. Разница счётчиков и есть число подтестов.

### Карточки: хуки сьюта

- **SetupSuite / TearDownSuite** — Один раз на прогон сьюта: дорогое и общее — соединение с БД, контейнер, сервер. Всё, что положили в поля, делится всеми методами.
- **SetupTest / TearDownTest** — Вокруг каждого метода: чистое состояние и уборка. TearDownTest вызывается даже при провале теста — testify держит его в defer.
- **SetupSubTest / TearDownSubTest** — Вокруг каждого s.Run: состояние и уборка подтеста. Между двумя подтестами одного метода проходят TearDownSubTest первого и SetupSubTest второго.
- **Порядок хуков** — SetupSuite → (SetupTest → метод → TearDownTest) для каждого метода → TearDownSuite. Методы идут в алфавитном порядке, а не по объявлению.
- **Ресурсы против данных** — Соединение и контейнеры — в SetupSuite, записи и файлы — в SetupTest: общее делить можно, данные нельзя.
- **TestMain** — Одна функция на пакет: управление запуском через m.Run() и os.Exit. Забытый m.Run() — зелёный отчёт без тестов. Нужда сьюта — хуки, нужда пакета — TestMain.

### Что дальше

Шесть хуков, порядок вызовов, деление «ресурсы в SetupSuite, данные в SetupTest», мутирующие поля и границы TestMain — с этим набором сьют перестаёт быть коробкой с полями-свалками и становится расписанием. Следующая ступень курса — табличные тесты и моки: сьют с хуками — готовый контейнер для того и другого, потому что подготовка уже вынесена из методов. Если базовая механика — методы без t, точка входа, s.Run — ещё тормозит, вернитесь к [основам сьютов](/courses/testify/lessons/11-suites-osnovy/): порядок хуков читается только поверх каркаса. Общий маршрут и соседние темы — на странице [testify-курса](/courses/testify/): после сьютов идут табличные тесты, где хуки соединяются со слайсом кейсов.

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

- Шесть хуков: 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.
