Содержание урока · 11 разделов
- assert.Panics: контракт «здесь обязано упасть»
- PanicsWithError: паники с error внутри
- Когда паника — контракт, а когда ошибка дизайна
- Ситуации и ожидаемые проверки
- recover в тестируемом коде и границы ответственности
- Паника на границах пакетов: кто фиксирует контракт
- Порядок работы с паникой в тесте
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
До этого урока все проверки testify работали с возвратами: функция вернула значение, сравнили, отметили провал. Паника ломает эту схему: выполнение функции обрывается, возврата нет, а без recover паника поднимается до самого тест-раннера и валит весь тест с малопонятным стеком. Поэтому проверить панику руками в тесте неудобно: нужна обёртка с defer/recover вокруг каждого подозрительного вызова. Testify прячет эту обёртку внутри своих утверждений: вы передаёте функцию без аргументов, testify вызывает её внутри собственного recover и проверяет, что паника случилась, что её значение совпало с ожидаемым или что это ошибка с нужным текстом. В этом уроке пройдём по трём проверкам семейства, поговорим о goroutine-границах и главное — о том, когда панику вообще стоит тестировать, а когда сам факт её проверки — сигнал переписать код.
assert.Panics: контракт «здесь обязано упасть»#
assert.Panics(t, f) вызывает f и ожидает, что внутри произойдёт паника — любая. Провал бывает двух направлений: f отработала без паники (отчёт покажет «func should panic»), или f вернулась нормально после recover внутри самой себя — то же самое для testify. Проверка полезна для контрактов, которые выражены паникой: конструктор NewRingBuffer(capacity) с отрицательной ёмкостью, метод At(index), обязанный отказываться на выходе за границы, парсер конфигурации, которому дали синтаксически невозможный документ. Во всех этих случаях паника — способ сказать «вызов сломан на этапе программирования, чините вызов, а не ловите ошибку в рантайме». У проверки есть уточнённые версии: PanicsWithValue(t, value, f) требует, чтобы значение паники совпало с ожидаемым через Equal, а PanicsWithError(t, msg, f) — чтобы паника несла ошибку с точным текстом Error()
Конструктор с паническим контрактом
GOfunc NewRingBuffer(capacity int) *RingBuffer {
if capacity <= 0 {
panic(fmt.Sprintf("capacity must be positive, got %d", capacity))
}
// ... остальной конструктор
}
func TestNewRingBuffer(t *testing.T) {
assert.Panics(t, func() { NewRingBuffer(0) })
assert.PanicsWithValue(t,
"capacity must be positive, got -3",
func() { NewRingBuffer(-3) },
)
}Первая проверка ловит сам факт паники, вторая — ещё и точное значение: fmt.Sprintf-строку, которую конструктор передал в panic. Второй вариант надёжнее: он защищает не только «падает», но и «падает по нужной причине», а не случайно из-за опечатки внутри конструктора.
PanicsWithError: паники с error внутри#
Проекты, которые паникуют ошибками (panic(err), где err реализует error), получают отдельную проверку: assert.PanicsWithError(t, "порт обязателен", f) распаковывает значение паники, проверяет, что оно реализует error, и сравнивает текст через err.Error(). Это удобнее, чем PanicsWithValue с ожидаемым объектом ошибки: создание «эталонной» ошибки в тесте требует доступа к конструктору, а текст — часть публичного контракта. Практическое правило: если ваша паника несёт error — берите PanicsWithError, если строку или любое другое значение — PanicsWithValue, если важен только факт — Panics. Каждая следующая проверка строже предыдущей, и в боевом коде чаще всего оправдана строгая: она не даст тесту «пройтись» по чужой случайной панике из-за unrelated-бага в соседней строке.
assert.Panics ожидает панику в той же горутине, где вызвана f. Если тестируемый код паникует внутри goroutine, которую вы запустили, — паника убивает всю программу, и никакой recover теста её не поймает. Тест с горутиной, чья паника «ожидается», заканчивается падением всего процесса, а не провалом проверки. Лечится это дизайном: паники не должны быть частью API, который работает в горутинах; ошибки там передаются каналами. Отдельно помните про требование FailNow-семантики: проверки require-пакета вызываются из потока теста, а не из горутин.
Когда паника — контракт, а когда ошибка дизайна#
Проверка паники в тесте — это фиксация контракта: «с этими аргументами вызывать нельзя, система встанет». Такой контракт уместен на границе программист-программист: конструкторы библиотечных типов, инварианты внутренних структур, конфигурация, которая разбирается один раз на старте. Там паника дешёвая и честная: ошибка обнаружится при первом же запуске тестов, а не в бою. Для всего остального в Go есть обычные ошибки, и их проверки разобраны в уроке про проверку ошибок. Совсем другая ситуация — данные пользователя, приходящие по сети, содержимое файлов, ответы API. Паника на таких входах — ошибка дизайна: внешние данные невалидны по определению, и код обязан возвращать ошибку, которую вызывающий обработает. Если тест на панику стоит над функцией, принимающей пользовательский ввод, — это повод переписать функцию, а не хвастаться проверкой. Граница простая: паника — для багов программиста, error — для мира вокруг.
Ситуации и ожидаемые проверки#
| Ситуация | Ожидаемое поведение | Проверка |
|---|---|---|
| Конструктор с инвариантом (ёмкость > 0) | Паника с описанием аргумента | PanicsWithValue |
| Паника несёт error-значение | Паника с err, у которого нужный текст | PanicsWithError |
| Лишь бы упало, значение не важно | Любая паника | Panics |
| Паника внутри goroutine | Падение всей программы | Так не тестируют — меняют дизайн |
| Невалидный ввод пользователя | Ошибка, а не паника | assert.Error, NoError — не Panics |
PanicsWithError на ошибке конфигурации
GOfunc MustParseConfig(raw string) *Config {
cfg, err := ParseConfig(raw)
if err != nil {
panic(fmt.Errorf("config: %w", err))
}
return cfg
}
func TestMustParseConfig(t *testing.T) {
assert.PanicsWithError(t,
"config: порт обязателен",
func() { MustParseConfig("{}") },
)
}Must-конвенция Go: функции с префиксом Must паникуют при ошибке. Для них PanicsWithError — естественная проверка: текст сообщения стабилен, и его поломка заметна в тесте раньше, чем в логах запуска.
recover в тестируемом коде и границы ответственности#
Иногда тестируемая функция сама ставит recover и превращает панику в ошибку — например, обёртка безопасных вызовов SafeCall. В таком случае assert.Panics ошибётся: паника снаружи не выйдет, и проверка провалится. Тестировать нужно публичное поведение: SafeCall возвращает ошибку с текстом паники — значит, проверка assert.ErrorContains, а не Panics. Правило для тестов простое: проверяйте то, что видит вызывающий, а не то, что происходит внутри. Это правило из мира моков применимо и здесь: контракт наружу важнее механики внутри, и именно наружный вид проверяют в чужих пакетах. Если внутренняя механика recover вам важна — тестируйте внутреннюю функцию напрямую, где паника ещё жива. И не забывайте про хвост msgAndArgs: как и у всех проверок testify, сюда можно передать сообщение с объяснением — оно попадёт в отчёт, если контракт вдруг нарушится.
Тот же контракт без testify
GOfunc TestNewRingBuffer_manual(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Fatal("ожидали панику на capacity 0")
}
}()
NewRingBuffer(0)
}Тот же контракт, что и у assert.Panics выше, — но обёртка с defer занимает больше места, не проверяет значение паники и вписывает провал в отчёт через t.Fatal. Ровно этот шаблон testify упаковывает в один вызов.
Паника на границах пакетов: кто фиксирует контракт#
У панического контракта есть владелец: пакет, который объявил инвариант. Тест на панику стоит в том же пакете — рядом с тестами конструктора — потому что он фиксирует внутреннее правило, а не поведение клиента. Внешние же тесты (интеграционные, из другого пакета) проверять панику редко бывают полезны: для клиента важны видимые последствия — возврат ошибки, нулевой объект, журнал. Если вы ловите себя на том, что панический тест лежит в чужом пакете, — это намёк, что контракт переехал: либо инвариант уже проверяется ошибкой, либо проверка дублирует внутреннюю и её можно убрать. Пара конструктор + Must-обёртка получает два теста: панический — на Must, обычный с ошибкой — на базовый конструктор. Так контракт читается прямо по файлу тестов, без чтения исходника.
Реальный сценарий: конструктор падал на capacity 0, тест фиксировал PanicsWithValue. Новичок «улучшил» код — вернул буфер с ёмкостью 1 и молча проглотил проблему. Тест покраснел в CI, и это спасло очередь задач: поведение «тихо создаём что-то не то» хуже явного отказа. Панический контракт в тесте — не формальность: он требует, чтобы изменение поведения было осознанным и прошло через правку теста, а не случилось между коммитами.
Порядок работы с паникой в тесте#
- Определите, паника это контракт или ошибка дизайна: программист промахнулся — паника, внешний ввод — error.
- Выберите строгость проверки: Panics, PanicsWithValue или PanicsWithError — по важности причины.
- Проверяйте в той же горутине: паника из чужой goroutine тест не поймает и уронит процесс.
- Обёртки с recover тестируйте по публичному поведению: ошибка вместо паники — ErrorContains.
- Не ставьте проверку паники над пользовательскими данными — это маркер дизайна, который надо чинить.
Проверка: горутины и паники
Тестируемый код запускает goroutine, внутри которой паникует. Что произойдёт с assert.Panics(t, func(){ ... go risky() ... })?
Проверка: выбор строгой проверки
Функция MustParseConfig паникует ошибкой fmt.Errorf("config: %w", ErrNoPort). Какая проверка надёжнее всего фиксирует контракт?
Что покажут три проверки?
Прочитайте код и выберите, что он выведет:
func boom() {
panic("провод оборван")
}
assert.Panics(t, boom)
assert.PanicsWithValue(t, "провод оборван", boom)
assert.PanicsWithError(t, "провод оборван", boom)Соберите проверки панического контракта
Допишите три проверки: факт паники, точное значение, ошибка с текстом.
Соберите тест Must-конструктора
Расставьте строки теста: объявление, проверка паники, и после — успешный путь.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: паники в тестах
Что дальше#
Панический контракт под контролем: Panics фиксирует факт, PanicsWithValue — значение, PanicsWithError — текст ошибки. Следующий вопрос — о словах: проверок много, и без сообщений отчёт превращается в перечисление провалов без объяснений. Хвост msgAndArgs, который есть у каждой проверки testify, заслуживает отдельного разговора — что писать, что не писать и как вспомогательные функции с t.Helper() делают сообщения дешёвыми. Напомню про среду: запустить проверки можно только в go test на локальном Go — браузерная песочница не исполняет пакеты тестирования; здесь вы можете потренировать порядок строк и пропуски. Общий маршрут курса — на странице курса по testify, а механика recover и panic без testify разобрана в уроке о паниках продвинутого курса.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- assert.Panics фиксирует факт паники, PanicsWithValue — значение через Equal, PanicsWithError — error с точным текстом.
- Чем строже проверка, тем надёжнее контракт: слабая Panics пройдёт по случайной панике из unrelated-бага.
- Паника не пересекает границу goroutine: recover теста её не поймает, процесс упадёт — это маркер дизайна.
- Паника — контракт для инвариантов программиста (конструкторы, Must-функции); на данных пользователя — ошибка дизайна.
- Must-конвенция: функции с префиксом Must паникуют при ошибке, и PanicsWithError — их естественная проверка.
- Функции с внутренним recover тестируют по публичному поведению: ошибка вместо паники — ErrorContains.
- Покрасневший тест на панику после «починки» кода — полезный сигнал: поведение изменилось неосознанно.
Домашнее задание
Возьмите конструктор структуры с инвариантом (например, NewStack с capacity > 0) и напишите три проверки: Panics на нулевой ёмкости, PanicsWithValue на отрицательной с точной строкой, PanicsWithError для версии с Must-обёрткой. Затем добавьте успешный путь и сравните, как отчёт различает строгую и нестрогую проверки, если намеренно поменяете текст паники.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем PanicsWithValue лучше обычной Panics?
Panics проходит по любой панике, включая случайную из бага в соседней строке — контракт при этом не проверен. PanicsWithValue сверяет значение паники с ожидаемым: тест краснеет, если изменился текст или тип, то есть при любом осознанном изменении поведения правка пройдёт через тест.
Как проверить панику, которая случается в goroutine?
Никак — и это правильно. Паника в goroutine убивает процесс, recover теста её не видит. Панический API в горутинах — ошибка проектирования: перепишите на ошибки в каналах или на безопасные обёртки, которые возвращают error, и тестируйте ошибку обычными проверками.
Функция ловит панику внутри и возвращает ошибку. Как её тестировать?
По публичному контракту: вызывающий видит ошибку, значит проверяйте assert.ErrorContains или EqualError на тексте. Паника осталась деталью реализации, и Panics на такой функции честно провалится — она уже не паникует наружу.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- msgAndArgs: свои сообщения в проверках
- Test suite: suite.Suite и его методы
- Хуки сьюта: SetupSuite, TearDownTest и компания
- Табличные тесты с testify
- True, False, Nil, NotNil и Empty: простые проверки
- InDelta и InEpsilon: числа с плавающей точкой
- ElementsMatch, Subset и Len: коллекции под контролем
- Сравнение структур и слайсов в тестах
- Все курсы по Go
Следующая ступень учебного пути
Viper: конфигурация приложений на Go
Интерактивный курс Viper для Go: YAML и env-переменные, приоритеты источников, Unmarshal, WatchConfig, профили окружений и секреты. 25 уроков с примерами.
- Собирать конфиг из файлов, окружения и флагов в один реестр
- Читать значения типизированно: Get, GetString, Unmarshal в структуры
- Задавать приоритеты источников: флаг > env > файл > default
- Перечитывать конфиг на лету через WatchConfig и OnConfigChange