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

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

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

Урок 3 из 25 ~18 мин +50 XP

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

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

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

Тест падает, и в отчёте паника там, где вы ждали аккуратный провал проверки. Знакомый сценарий: функция распарсила битый ответ, вернула nil вместо структуры и ошибку, но тест не остановился — assert лишь пометил провал и пошёл дальше, на следующей строке полез в поля nil-структуры и рухнул. В отчёте теперь не «строка 12: имя не совпало», а стек паники из глубины пакета, и разбираться в нём сложнее, чем чинить сам баг. Именно от этого сценария в testify живёт второй пакет — require. В уроке про первый 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.

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 построчно#

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

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

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

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

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

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

Прочитайте код и выберите, что он выведет:

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, итоговые проверки через assert.

func TestParseConfig(t *testing.T) { cfg, err := ParseConfig(raw) require.(t, err) require.(t, cfg) assert.(t, 5432, cfg.Port) }

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

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

Строки

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

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

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

Термин

assert.Equal при провале

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

t.Errorf: тест помечен проваленным, выполнение продолжается, отчёт собирает все провалы за прогон.

1 / 6

Что дальше#

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

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

  • 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, с изменённым предусловием и без.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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