---
title: "assert и require: когда тест должен падать сразу"
description: "assert vs require в testify: провал assert продолжает тест, require останавливает через FailNow. Правило require для предусловий, ловушка горутин и примеры."
course: "Testify: тестирование Go с assert и require"
url: "https://golangmaster.ru/courses/testify/lessons/03-assert-require/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# assert и require: когда тест должен падать сразу

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

Выбор assert vs require выглядит придиркой: наборы функций у пакетов идентичные. Разница проявляется в худший момент — тест продолжил работу после проваленной подготовки и упал паникой по nil. Разберём механику Errorf и FailNow, правило require для предусловий и ловушку горутин.

Тест падает, и в отчёте паника там, где вы ждали аккуратный провал проверки. Знакомый сценарий: функция распарсила битый ответ, вернула nil вместо структуры и ошибку, но тест не остановился — assert лишь пометил провал и пошёл дальше, на следующей строке полез в поля nil-структуры и рухнул. В отчёте теперь не «строка 12: имя не совпало», а стек паники из глубины пакета, и разбираться в нём сложнее, чем чинить сам баг. Именно от этого сценария в testify живёт второй пакет — require. В уроке про [первый assert](/courses/testify/lessons/02-pervyj-assert/) мы уже гоняли assert.Equal через go test; здесь разберём, чем выбор assert vs require меняет судьбу провала, почему у пакетов одинаковый набор функций и в каких местах require — единственный правильный ответ. Полные прогоны, как и весь курс, выполняются локальным go test: браузерная песочница пакеты тестирования не исполняет, зато каждый вывод ниже проверяется одной командой.

### assert vs require: одна ошибка, две судьбы

Снаружи пакеты близнецы: у обоих есть Equal, NoError, NotNil и весь остальной набор, сигнатуры совпадают до символа — сначала t, потом ожидаемое значение, потом фактическое. Отличие спрятано в том, что функция вызывает при провале. assert.Equal при неудаче вызывает t.Errorf: тест помечается проваленным, но выполнение идёт дальше — следующие проверки выполняются, и отчёт собирает все красные строки за один прогон. require.Equal при той же неудаче вызывает t.FailNow: текущая тест-функция останавливается немедленно, всё, что написано после, не выполняется, а тест падает с единственным сообщением. Никакой магии внутри: require — тонкая обёртка над assert, которая после проваленной проверки дёргает FailNow. Отсюда два практических вывода. Мигрировать между пакетами — значит поменять один импорт. И если тест вдруг начал обрываться на середине без видимой причины, первым делом проверьте, не сменили ли вы в нём assert на require.

### Один и тот же тест в двух пакетах

```go
func TestParseUser_assert(t *testing.T) {
        user, err := ParseUser(brokenJSON)

        assert.NoError(t, err)             // провал: тест помечен, работа идёт дальше
        assert.Equal(t, "anna", user.Name) // user == nil -> паника
}

func TestParseUser_require(t *testing.T) {
        user, err := ParseUser(brokenJSON)

        require.NoError(t, err)             // провал: тест остановлен здесь
        require.Equal(t, "anna", user.Name) // строка не выполнится никогда
}
```

ParseUser на битом JSON возвращает nil и ошибку. В assert-версии первая проверка честно красит тест, но выполнение продолжается, и вторая строка разыменовывает nil — тест падает паникой вместо аккуратного отчёта. В require-версии тест завершается на первой же проверке с одним ясным сообщением про ошибку парсинга.

### Что именно делает t.FailNow

FailNow — метод стандартного testing.T, testify его не изобретал: пакет require вызывает то, что фреймворк предлагает из коробки. Сначала вызов помечает тест проваленным — статус такой же, как у t.Errorf. Затем останавливает текущую тест-функцию через runtime.Goexit: горутина теста завершается, отложенные defer успевают отработать, а фреймворк переходит к следующей тест-функции пакета. Важная деталь: останавливается ровно одна функция. Провал require в TestParseUser не отменяет TestParseConfig и остальных соседей — их отчёт собирается целиком. Для require в Go-тестах это и делает предусловия осмысленными: сломанная подготовка выключает текущий тест, не трогая остальные. Ещё одна деталь, отличающая require: его TestingT требует от t не только Errorf, но и FailNow — *testing.T подходит, а произвольный тип с одним Errorf, который assert принимает охотно, require уже не устроит. Для обычных тестов разницы нет, но она всплывает в генераторах и обёртках над t.

> **Паника вместо провала: как мусорится отчёт.** Живой кейс из ревью: тест вызывал assert.NoError на ошибке парсинга, а затем проверял семь полей структуры. На битых данных структура была nil, тест честно пометил первую проверку красным — и полез дальше, а на втором поле упал паникой. Красным становился весь тест, но единственное настоящее сообщение — про ошибку парсинга — тонул в стеке, а шесть проверок, которые могли бы отработать, не выполнились вовсе. Автор потратил вечер на поиски баги в пакете, хотя баг жил в самом тесте: проверку nil поставили не тем пакетом. Один require.NoError перед работой с данными сокращает отчёт до одной честной строки.

### require готовит, assert проверяет

Из двух судеб провала вырастает рабочее правило, которое в командах звучит почти как мантра: require — для подготовки, assert — для проверок. Пока тест готовит данные — создаёт сервис, делает запрос, парсит ответ — любой провал делает бессмысленным всё, что дальше: проверять поля структуры, которой нет, мы уже пробовали выше. Поэтому каждое предусловие закрывается require: require.NoError на ошибке, require.NotNil на указателе, require.Len на слайсе, если проверки завязаны на его размер. Как только подготовка закончилась, наступает очередь assert: сами проверки результата живут в нём, и провал любой из них не прячет остальные — один прогон показывает все расхождения сразу. Для функции, возвращающей структуру с полями, картина типовая: две require-строки предусловий, затем пачка assert-строк по полям. Если подготовка состоит из одного шага, а проверок всего две, разницы вы не заметите — правило экономит время на тестах, где шагов больше.

### Типовой каркас: предусловия и проверки

```go
func TestGetUser(t *testing.T) {
        repo := newTestRepo(t)

        user, err := repo.Get(42)
        require.NoError(t, err) // подготовка: без пользователя проверять нечего
        require.NotNil(t, user) // и nil-указателя быть не должно

        assert.Equal(t, "anna", user.Name)
        assert.Equal(t, 29, user.Age)
        assert.False(t, user.Deleted)
}
```

Две require-строки образуют предусловие: тест продолжится только при живом user. Ниже идут три проверки результата в assert — провал любой из них попадёт в отчёт, но не остановит соседей: если имя и возраст не совпали, вы увидите оба расхождения за один прогон go test.

У правила есть оппоненты, и их позиция не безосновательна: часть команд пишет весь тест на require. Аргумент короткий — провал должен означать провал, а отчёт из одного первого сообщения короче и не отвлекает. На маленьких тестах с двумя-тремя проверками я с этим согласен: разницы не видно ни в отчёте, ни в скорости. Но проверок становится пять-семь — и require-везде превращает отладку в цикл: прогон, один провал, правка, прогон, следующий провал. assert на проверках результата отдаёт всю карту расхождений сразу, и стоимость итерации падает. Компромисс, который живёт в моих тестах: require на всём, что может уронить тест паникой, assert на всём, что просто не совпало. Граница применимости простая: как только между провалом и его последствиями стоит разыменование указателя, это уже не assert-вопрос, а require.

### require и горутины: FailNow не оттуда

У FailNow есть жёсткое ограничение, о котором документация предупреждает прямо: вызывать его можно только из горутины, которая запускает тест-функцию. Фреймворк считает текущим тестом лишь главный поток — require, вызванный из запущенной вами горутины, вызовет FailNow там, и завершится сама горутина, а тест продолжит работать, как ни в чём не бывало. Хуже того: если тест ждал эту горутину по каналу, он может зависнуть навсегда, и go test закончится по глобальному таймауту с невнятной диагностикой вместо провала. assert из горутин вызывать можно: t.Error, на котором он построен, безопасен для конкурентных вызовов из любого числа горутин. Практичная схема — рабочие горутины только производят данные, а проверки живут в главном потоке после синхронизации. Если провал нужно зафиксировать внутри горутины, используйте assert и доведите горутину до конца: defer wg.Done() или закрытие канала.

### Проверки после синхронизации

```go
func TestFetcher(t *testing.T) {
        out := make(chan string, len(urls))

        go func() {
                defer close(out)
                for _, u := range urls {
                        body, err := fetch(u)

                        assert.NoError(t, err) // t.Errorf безопасен из горутин
                        if err == nil {
                                out <- body
                        }
                }
        }()

        var got []string
        for body := range out {
                got = append(got, body)
        }

        assert.Equal(t, len(urls), len(got)) // итог — в главном потоке
}
```

Провал внутри горутины фиксируется assert-ом: под ним стоит t.Errorf, который можно вызывать конкурентно, и тест продолжит работу. require здесь запрещён — FailNow завершил бы горутину, а тест остался бы ждать close вечно. Итоговая проверка идёт уже после range по каналу, в главном потоке, где require тоже безопасен.

### Пять правил, как разделить пакеты

- Ошиблась подготовка — тест не имеет смысла: require.NoError, require.NotNil и прочие предусловия ставьте первыми.
- Проверки результата пишите в assert: все расхождения окажутся в отчёте за один прогон.
- Перед любым обращением к полю структуры или методу указателя — require, иначе паника перепишет отчёт.
- Из горутин вызывайте только assert: FailNow обязан жить в goroutine теста.
- Переход между пакетами — смена одного импорта: наборы функций идентичны, сигнатуры совпадают.

### assert против require построчно

| Свойство | assert | require |
| --- | --- | --- |
| Пакет | testify/assert | testify/require |
| Набор функций | полный: Equal, NoError, NotNil и остальные | идентичный assert — различий в сигнатурах нет |
| Вызов при провале | t.Errorf | t.FailNow |
| Что с тестом | помечен проваленным, выполнение продолжается | остановлен немедленно |
| Остальные проверки | выполняются и попадают в отчёт | не выполняются |
| Куда ставить | проверки результата | предусловия и подготовку |
| Из горутин | можно: t.Errorf безопасен конкурентно | нельзя: FailNow требует goroutine теста |

### Проверка: судьба теста после require

**Вопрос:** В тесте пять строк, на третьей провалился require.Equal. Что покажет прогон?

- ✅ Тест остановлен на третьей строке: помечен проваленным, четвёртая и пятая не выполнены
- Тест продолжится: require помечает провал, как assert, и идёт дальше
- Тест перезапустится с первой строки и остановится снова
- Провал проигнорирован: require только печатает сообщение

Провал require завершает тест-функцию через t.FailNow: пометка провала плюс немедленная остановка. Соседние тест-функции пакета при этом выполняются как обычно — радиус остановки ограничен текущей функцией.

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

**Вопрос:** Тест запустил горутину-воркера, и из неё вызвали require.Equal, который провалился. Что произойдёт?

- Весь тест остановится немедленно, как и при вызове из главного потока
- ✅ Завершится только горутина: тест продолжит работать и может зависнуть или упасть позже
- require вызовет панику по всему тестовому бинарнику
- Провал не будет зафиксирован: горутины не могут отмечать ошибки

FailNow обязан вызываться из goroutine теста; в чужой горутине он завершает её саму, а фреймворк об этом не узнает. Для горутин безопасен assert — t.Error допускает конкурентные вызовы — плюс синхронизация результатов с главным потоком.

### Сколько строк выполнится?

```go
func TestService(t *testing.T) {
        // NewService на битом конфиге вернёт nil и ошибку
        svc, err := NewService(brokenConfig)

        require.NoError(t, err)
        assert.True(t, svc.Ready())
        assert.Equal(t, "prod", svc.Env)
}
```

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

- ✅ Тест остановлен на require.NoError: обе assert-строки не выполнятся
- Три провала в отчёте: require и два assert
- Паника на svc.Ready(): assert продолжил работу после провала require
- Тест зелёный: require не останавливает тест

require.NoError останавливает тест немедленно — до svc.Ready() дело не доходит, поэтому паники нет, а в отчёте ровно одно сообщение про неожиданную ошибку. Поменяйте require на assert — и картина изменится на панику из первого же вызова по nil.

### Восстановите предусловия

Допишите проверки: подготовка через require, итоговые проверки через assert.

```go
func TestParseConfig(t *testing.T) {
        cfg, err := ParseConfig(raw)

        require.___(t, err)
        require.___(t, cfg)

        assert.___(t, 5432, cfg.Port)
}
```

Подсказка: Предусловие — отсутствие ошибки и живой указатель; порт сверяется с ожидаемым через Equal.

Правильные ответы в пропусках: `NoError`, `NotNil`, `Equal`

> **require внутри t.Run: радиус ещё меньше.** В табличных тестах каждый сабтест — отдельная тест-функция со своим t, поэтому require.NoError в одном кейсе остановит только его. Остальные кейсы пройдут по расписанию, и отчёт покажет все красные строки таблицы сразу. Это делает require в сабтестах безопасным и удобным — подробный разбор конструкции ждёт в уроке про табличные тесты.

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

Расставьте строки: вызов, два предусловия, проверка результата и закрывающая скобка.

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

1. `func TestGetUser(t *testing.T) {`
2. `user, err := repo.Get(42)`
3. `require.NoError(t, err)`
4. `require.NotNil(t, user)`
5. `assert.Equal(t, "anna", user.Name)`
6. `}`

### Карточки: assert и require

- **assert.Equal при провале** — t.Errorf: тест помечен проваленным, выполнение продолжается, отчёт собирает все провалы за прогон.
- **require.Equal при провале** — t.FailNow: тест-функция останавливается немедленно, строки ниже не выполняются, соседние тесты не затронуты.
- **t.FailNow** — Пометить провал и завершить текущую тест-функцию через runtime.Goexit. Вызывать только из goroutine теста.
- **require в горутине** — Запрещён: FailNow завершит горутину, а не тест. В горутинах — assert, результаты синхронизируются каналом или WaitGroup.
- **Правило выбора** — require — подготовка и предусловия, assert — проверки результата. Набор функций у пакетов идентичный.
- **require внутри t.Run** — Останавливает только сабтест: у каждого кейса свой t, остальные кейсы таблицы отрабатывают целиком.

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

Каркас с предусловиями — половина пайплайна; вторая половина — что именно проверять, когда функция возвращает ошибку. Это тема следующего урока про [assert.NoError, EqualError и ErrorIs](/courses/testify/lessons/04-error-assertions/): там же разберём, почему assert.Equal на обёрнутых ошибках подводит. Чтобы закрепить этот урок, прогоните примеры локальным go test: закомментируйте require.NotNil в каркасе TestGetUser и посмотрите, как паника переписывает отчёт — увидеть это своими глазами полезнее любых слов. Проверки не заканчиваются на Equal: у него внутри reflect.DeepEqual, и тонкости сравнения структур со вложенными полями разобраны в уроке про [сравнение структур и слайсов](/courses/testify/lessons/05-sravnenie-struktur/). Полный маршрут курса — на странице [testify-курса](/courses/testify/).

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

- assert и require — два пакета с идентичным набором функций; отличается только реакция на провал.
- Провал assert вызывает t.Errorf: тест помечен красным, выполнение продолжается, отчёт собирает все провалы.
- Провал require вызывает t.FailNow: тест-функция останавливается немедленно, следующие строки не выполняются.
- t.FailNow обязан вызываться из goroutine теста: в горутинах require не останавливает тест — там безопасен только assert.
- Рабочее правило: require — предусловия и подготовка, assert — проверки результата.
- require.NoError и require.NotNil перед работой с данными спасают от паники по nil и замусоренных отчётов.
- Внутри t.Run остановка require затрагивает только сабтест: остальные кейсы таблицы отрабатывают.

## Домашнее задание

Возьмите тест из урока про первый assert и расставьте require и assert заново: предусловия — через require.NoError и require.NotNil, проверки полей — через assert. Затем вынесите одну проверку в горутину и убедитесь, почему require там не останавливает тест. Прогон — локальным go test, с изменённым предусловием и без.

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

### Почему require нельзя вызывать из горутин?

t.FailNow останавливает только ту горутину, из которой вызван, а фреймворк считает текущей лишь goroutine теста. Вызов из сторонней горутины завершит её саму: тест продолжит работать и зависнет или упадёт позже. В горутинах используют assert — t.Error безопасен для конкурентных вызовов.

### Одинаковый ли набор функций у assert и require?

Да, пакеты генерируются из одного набора: Equal, NoError, NotNil, ErrorIs и остальные присутствуют в обоих. Разница только в поведении при провале: assert вызывает t.Errorf и продолжает, require — t.FailNow и останавливает. Переход между пакетами — это смена одного импорта.

### Можно ли писать весь тест на require?

Можно, и часть команд так делает: каждый провал останавливает тест, отчёт короче. Цена — вы видите только первый провал за прогон, остальные проверки молчат до следующего исправления. Для тестов с двумя-тремя проверками разницы почти нет; для широких проверок результата assert информативнее.
