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

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

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

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

Проверка паник: Panics и PanicsWithValue

В Go паника — не исключение для обработки, а аварийный сигнал: логическая ошибка программиста, за которую код не отвечает gracefully. Но бывают места, где паника — задокументированный контракт: конструктор, обязанный отказываться от невалидных аргументов, парсер, который не умеет падать тихо. Testify даёт для таких мест свои проверки: assert.Panics и его уточнения. Разберём, как ловить паники в тестах и где проходит граница между проверяемым контрактом и ошибкой дизайна.

Содержание урока · 11 разделов

До этого урока все проверки 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()

Конструктор с паническим контрактом

GO
func 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-бага в соседней строке.

Когда паника — контракт, а когда ошибка дизайна#

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

Ситуации и ожидаемые проверки#

СитуацияОжидаемое поведениеПроверка
Конструктор с инвариантом (ёмкость > 0)Паника с описанием аргументаPanicsWithValue
Паника несёт error-значениеПаника с err, у которого нужный текстPanicsWithError
Лишь бы упало, значение не важноЛюбая паникаPanics
Паника внутри goroutineПадение всей программыТак не тестируют — меняют дизайн
Невалидный ввод пользователяОшибка, а не паникаassert.Error, NoError — не Panics

PanicsWithError на ошибке конфигурации

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

GO
func 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, обычный с ошибкой — на базовый конструктор. Так контракт читается прямо по файлу тестов, без чтения исходника.

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

  • Определите, паника это контракт или ошибка дизайна: программист промахнулся — паника, внешний ввод — 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)

Соберите проверки панического контракта

Допишите три проверки: факт паники, точное значение, ошибка с текстом.

assert.(t, func() { NewRingBuffer(0) }) assert.PanicsWith(t, "capacity must be positive, got -1", func() { NewRingBuffer(-1) }) assert.PanicsWith(t, "config: порт обязателен", func() { MustParseConfig("{}") })

Соберите тест Must-конструктора

Расставьте строки теста: объявление, проверка паники, и после — успешный путь.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: паники в тестах

Термин

assert.Panics(t, f)

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

Проверяет, что функция f паникует в текущей горутине. Самая слабая проверка семейства: значение паники не смотрит.

1 / 6

Что дальше#

Панический контракт под контролем: 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-обёрткой. Затем добавьте успешный путь и сравните, как отчёт различает строгую и нестрогую проверки, если намеренно поменяете текст паники.

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

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

Чем PanicsWithValue лучше обычной Panics?

Panics проходит по любой панике, включая случайную из бага в соседней строке — контракт при этом не проверен. PanicsWithValue сверяет значение паники с ожидаемым: тест краснеет, если изменился текст или тип, то есть при любом осознанном изменении поведения правка пройдёт через тест.

Как проверить панику, которая случается в goroutine?

Никак — и это правильно. Паника в goroutine убивает процесс, recover теста её не видит. Панический API в горутинах — ошибка проектирования: перепишите на ошибки в каналах или на безопасные обёртки, которые возвращают error, и тестируйте ошибку обычными проверками.

Функция ловит панику внутри и возвращает ошибку. Как её тестировать?

По публичному контракту: вызывающий видит ошибку, значит проверяйте assert.ErrorContains или EqualError на тексте. Паника осталась деталью реализации, и Panics на такой функции честно провалится — она уже не паникует наружу.

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

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

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

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

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

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

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