---
title: "Проверка паник: Panics и PanicsWithValue"
description: "assert.Panics, PanicsWithValue и PanicsWithError: тесты на код, который обязан паникать. Когда паника — контракт, а когда ошибка дизайна, с примерами."
course: "Testify: тестирование Go с assert и require"
url: "https://golangmaster.ru/courses/testify/lessons/09-panics/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

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

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

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

До этого урока все проверки 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-бага в соседней строке.

> **Паника не пересекает границу goroutine.** assert.Panics ожидает панику в той же горутине, где вызвана f. Если тестируемый код паникует внутри goroutine, которую вы запустили, — паника убивает всю программу, и никакой recover теста её не поймает. Тест с горутиной, чья паника «ожидается», заканчивается падением всего процесса, а не провалом проверки. Лечится это дизайном: паники не должны быть частью API, который работает в горутинах; ошибки там передаются каналами. Отдельно помните про требование FailNow-семантики: проверки require-пакета вызываются из потока теста, а не из горутин.

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

Проверка паники в тесте — это фиксация контракта: «с этими аргументами вызывать нельзя, система встанет». Такой контракт уместен на границе программист-программист: конструкторы библиотечных типов, инварианты внутренних структур, конфигурация, которая разбирается один раз на старте. Там паника дешёвая и честная: ошибка обнаружится при первом же запуске тестов, а не в бою. Для всего остального в Go есть обычные ошибки, и их проверки разобраны в уроке про [проверку ошибок](/courses/testify/lessons/04-error-assertions/). Совсем другая ситуация — данные пользователя, приходящие по сети, содержимое файлов, ответы 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](/courses/testify/lessons/10-soobshcheniya/): как и у всех проверок 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, обычный с ошибкой — на базовый конструктор. Так контракт читается прямо по файлу тестов, без чтения исходника.

> **Панику «починили», тест покраснел — и оказался прав.** Реальный сценарий: конструктор падал на capacity 0, тест фиксировал PanicsWithValue. Новичок «улучшил» код — вернул буфер с ёмкостью 1 и молча проглотил проблему. Тест покраснел в CI, и это спасло очередь задач: поведение «тихо создаём что-то не то» хуже явного отказа. Панический контракт в тесте — не формальность: он требует, чтобы изменение поведения было осознанным и прошло через правку теста, а не случилось между коммитами.

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

- Определите, паника это контракт или ошибка дизайна: программист промахнулся — паника, внешний ввод — error.
- Выберите строгость проверки: Panics, PanicsWithValue или PanicsWithError — по важности причины.
- Проверяйте в той же горутине: паника из чужой goroutine тест не поймает и уронит процесс.
- Обёртки с recover тестируйте по публичному поведению: ошибка вместо паники — ErrorContains.
- Не ставьте проверку паники над пользовательскими данными — это маркер дизайна, который надо чинить.

### Проверка: горутины и паники

**Вопрос:** Тестируемый код запускает goroutine, внутри которой паникует. Что произойдёт с assert.Panics(t, func(){ ... go risky() ... })?

- Проверка сработает: testify ловит паники в любой горутине
- ✅ Паника убьёт весь процесс — recover теста её не поймает
- Тест продолжится, goroutine молча прекратится
- Проверка вернёт false без последствий

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

### Проверка: выбор строгой проверки

**Вопрос:** Функция MustParseConfig паникует ошибкой fmt.Errorf("config: %w", ErrNoPort). Какая проверка надёжнее всего фиксирует контракт?

- assert.Panics: главное, что падает
- ✅ assert.PanicsWithError(t, "config: порт обязателен", fn): фиксирует и факт, и текст
- assert.Error: Must-функции возвращают ошибки
- assert.Empty: паника ничего не возвращает

PanicsWithError распаковывает error-значение паники и сверяет текст Error(). Panics слишком слаба — пройдёт по любой случайной панике, а Must-функции ошибок не возвращают, поэтому Error тут неприменим.

### Что покажут три проверки?

```go
func boom() {
        panic("провод оборван")
}

assert.Panics(t, boom)
assert.PanicsWithValue(t, "провод оборван", boom)
assert.PanicsWithError(t, "провод оборван", boom)
```

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

- Все три пройдут: значение — строка, она же текст ошибки
- ✅ Первые две пройдут, третья провалится: значение паники не реализует error
- Первая пройдёт, остальные провалятся: Panics не смотрит значение
- Все провалятся: строку нельзя передать в panic

Panics довольствуется фактом паники, PanicsWithValue сравнивает значение со строкой «провод оборван» — совпадает. PanicsWithError требует, чтобы значение паники реализовывало error, а здесь это голая строка — проверка честно провалится.

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

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

```go
assert.___(t, func() { NewRingBuffer(0) })

assert.PanicsWith___(t,
        "capacity must be positive, got -1",
        func() { NewRingBuffer(-1) })

assert.PanicsWith___(t,
        "config: порт обязателен",
        func() { MustParseConfig("{}") })
```

Подсказка: Факт — Panics; строка-значение — PanicsWithValue; паника с error — PanicsWithError.

Правильные ответы в пропусках: `Panics`, `Value`, `Error`

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

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

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

1. `func TestMustParseConfig(t *testing.T) {`
2. `assert.PanicsWithError(t, "config: порт обязателен", func() { MustParseConfig("{}") })`
3. `cfg := MustParseConfig("{\"port\": 8080}")`
4. `assert.Equal(t, 8080, cfg.Port)`
5. `}`

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

- **assert.Panics(t, f)** — Проверяет, что функция f паникует в текущей горутине. Самая слабая проверка семейства: значение паники не смотрит.
- **PanicsWithValue(t, value, f)** — Фиксирует и факт паники, и её значение через Equal. Защищает от «падает, но не по той причине».
- **PanicsWithError(t, msg, f)** — Значение паники обязано реализовывать error с точным текстом Error(). Для Must-функций, паникующих ошибками.
- **Паника и goroutine** — recover ловит панику только в своей горутине. Паника в goroutine убивает процесс — такие API чинят дизайном, а не тестом.
- **Паника как контракт** — Уместна для инвариантов программиста: конструкторы, Must-функции, конфигурация на старте. На данных пользователя — ошибка дизайна: там error.
- **recover внутри тестируемого кода** — Если функция сама превращает панику в ошибку, Panics провалится. Проверяйте публичное поведение: assert.ErrorContains вместо Panics.

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

Панический контракт под контролем: Panics фиксирует факт, PanicsWithValue — значение, PanicsWithError — текст ошибки. Следующий вопрос — о словах: проверок много, и без сообщений отчёт превращается в перечисление провалов без объяснений. Хвост msgAndArgs, который есть у каждой проверки testify, заслуживает отдельного разговора — что писать, что не писать и как вспомогательные функции с t.Helper() делают сообщения дешёвыми. Напомню про среду: запустить проверки можно только в go test на локальном Go — браузерная песочница не исполняет пакеты тестирования; здесь вы можете потренировать порядок строк и пропуски. Общий маршрут курса — на странице [курса по testify](/courses/testify/), а механика recover и panic без testify разобрана в [уроке о паниках](/courses/go-advanced/lessons/03-panic-recover/) продвинутого курса.

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

- 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 на такой функции честно провалится — она уже не паникует наружу.
