---
title: "Test suite: suite.Suite и его методы"
description: "Test suite в testify: структура со встраиванием suite.Suite, методы TestXxx без t, запуск suite.Run и s.Run, одна точка входа и рефлексия под капотом."
course: "Testify: тестирование Go с assert и require"
url: "https://golangmaster.ru/courses/testify/lessons/11-suites-osnovy/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

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

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

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

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

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

Сьют — обычная структура Go, в которую встроен тип suite.Suite из пакета github.com/stretchr/testify/suite: type StoreSuite struct { suite.Suite }. Встраивание — тот же механизм, что у структур из [базового курса](/courses/go-basics/lessons/23-struktury/): методы 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 из [урока про выбор пакета](/courses/testify/lessons/03-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). Что случится при прогоне?

- Запустится как обычный тест: testify передаст ему t
- ✅ Testify пометит метод как тест со сломанной сигнатурой и напишет это в отчёт
- Метод молча не запустится, а go test останется зелёным
- Компилятор откажется собирать пакет

Рефлексия проверяет сигнатуру: метод с входными параметрами или возвращаемыми значениями не запускается — вместо него в отчёте появляется ошибка invalid signature с числом лишних параметров. Молчаливый пропуск случается в другой ситуации: метод без префикса Test рефлексия вообще не находит. Компилятору такая сигнатура не мешает — это валидный метод.

### Запуск: 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?

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

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

- ✅ Один тест верхнего уровня: TestCalcSuite, а методы сьюта — подтесты внутри него
- Два теста: TestAdd и TestSub
- Три теста: TestCalcSuite, TestAdd и TestSub
- Тестов нет: prepare не скомпилируется

go test собирает только Test-функции пакета, а здесь она одна. Внутри suite.Run методы TestAdd и TestSub выполняются подтестами, и -v покажет три строки RUN: родитель и два метода. prepare без префикса Test для рефлексии не существует — это обычный приватный метод.

### 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) провалилась. Что произойдёт?

- Остановится весь метод TestTotal вместе со следующими подтестами
- ✅ Остановится только подтест «скидка»; следующий s.Run продолжится
- Провалится весь сьют, остальные методы не запустятся
- Require не работает внутри s.Run и вызовет панику

s.Require() привязан к текущему t, а внутри подтеста это t именно подтеста: FailNow прерывает только его. Метод продолжит следующие s.Run, остальные методы сьюта тоже выполнятся. Тот же принцип, что у t.Run в плоских тестах: рамки прерывания задаёт ближайший подтест.

> **Спор: сьют против плоских функций.** Самый частый аргумент против: методы-тесты собираются рефлексией, и часть тестирования становится неявной. Точка входа одна, порядок методов задаёт алфавит, а не глаз разработчика, хуки прячут подготовку — и тест, который «просто зелёный», требует чтения родительской структуры. Плоские функции честнее: каждая — самодостаточный тест с явным t. Я всё же держу сьюты там, где общий контекст дорогой: соединение, контейнер, тестовый сервер поднимаются один раз на десяток методов, и экономия реальна. Граница простая: чистые функции и перебор кейсов — плоские тесты; модуль с состоянием и дорогой подготовкой — сьют. Спорить о вкусе можно бесконечно, а рабочий критерий один: сколько раз пришлось скопировать подготовку.

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

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

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

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

> **Пакет зелёный, а тестов ноль.** История из ревью: метод TestCreate переименовали в prepareCreate, пакет пересобрался, go test ответил ok — и никто не заметил, что сьют больше не содержит тестов. Рефлексия отбирает только экспортированные методы с префиксом Test; всё остальное для неё невидимо. А suite.Run, не нашедший ни одного метода, завершается молча — даже SetupSuite в этом случае не вызовется, библиотека возвращает управление раньше хуков. Лечится гигиеной: после переименований смотреть в вывод go test -v и проверять, что строки подтестов на месте, а не только общий PASS.

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

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

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

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

```go
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; точка входа зовёт suite.Run; require-версия появляется через s.Require().

Правильные ответы в пропусках: `suite.Suite`, `Run`, `Require`

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

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

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

1. `func (s *CartSuite) TestAddTwo() {`
2. `cart := NewCart()`
3. `cart.Add("apple")`
4. `s.Equal(1, cart.Count("apple"))`
5. `cart.Add("pear")`
6. `s.Len(cart.Items(), 2)`

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

- **suite.Suite** — Тип из пакета github.com/stretchr/testify/suite, встраиваемый в структуру-сьют: приносит T(), Require(), Assert() и поле *assert.Assertions для прямых вызовов вида s.Equal.
- **Метод-тест сьюта** — func (s *Suite) TestX() — без *testing.T и без возвращаемых значений; testify собирает такие методы рефлексией и запускает каждый через t.Run.
- **suite.Run(t, new(MySuite))** — Единая точка входа: подключает t, находит методы-тесты, выполняет каждый подтестом с именем MySuite/TestX. При нуле найденных методов завершается без хуков.
- **s.T()** — Текущий *testing.T сьюта: Deadline, TempDir и всё, что требует t напрямую. Внутри s.Run на время подтеста подменяется.
- **s.Require() и s.Assert()** — Доступ к require- и assert-контексту: Require().NoError останавливает метод при провале, прямой s.Equal — только помечает. До suite.Run Require() паникует.
- **s.Run(name, func())** — Подтест сьюта: замыкание без параметров, вокруг работают SetupSubTest и TearDownSubTest, паника перехватывается testify, имя входит в путь -run.

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

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

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

- Сьют — структура со встраиванием 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 добирается до одного из них.

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

### Можно ли объявить метод-тест с параметром 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 даёт подтесты, но не общий контекст: подготовка остаётся внутри каждой функции. Сьют добавляет поля структуры для состояния и хуки подготовки: дорогое и общее создаётся один раз, методы получают готовый мир. Для чистых функций разницы почти нет — там плоские тесты проще.
