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

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

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

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

Проверка ошибок: Error, NoError и EqualError

Проверка ошибок в Go-тестах — половина всех строк, и testify закрывает её пятью вызовами: assert.NoError, assert.Error, EqualError и ErrorIs. Разберём, какая проверка для какой ситуации, чем NoError яснее Nil, что дают sentinel и обёртки и почему Equal красит тесты на обёрнутых ошибках.

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

Проверка ошибок в Go-тестах сводится к пяти вызовам — assert.NoError, assert.Error, assert.EqualError, assert.ErrorIs и assert.ErrorContains, — и они закрывают практически весь конвейер, от «ошибки быть не должно» до «sentinel найден сквозь три обёртки». Ручной вариант все помнят из базового курса: if err != nil { t.Fatalf(...) } — лесенка, которая на пятой проверке съедает экран и читается хуже кода, который она проверяет. Testify сжимает каждую развилку до одной строки, а отчёт при провале печатает то, что вернула функция, — диагноз ставится без открытия исходников. Разберём каждую проверку по назначению, сравним NoError с Nil — там прячется ловушка отчёта — и поймём, почему assert.Equal, который кажется универсальным, красит тесты на обёрнутых ошибках без вины кода. Предусловия мы в прошлом уроке отдали require: тот же принцип распространяется и на ошибки.

assert.NoError и assert.Error: базовая пара#

Две проверки, на которых держится большинство тестов. assert.NoError(t, err) проваливает тест, если err не nil, и при провале печатает текст самой ошибки: Received unexpected error и ниже то, что вернула функция. assert.Error(t, err) — зеркальный случай: тест красится, если err внезапно nil, то есть функция не отреагировала на заведомо плохой вход. Обе принимают только t, err и опциональное сообщение из семейства msgAndArgs — им нечего сравнивать, им важно лишь наличие или отсутствие значения. Второй аргумент-сообщение выручает, когда по отчёту неочевидно, какая из ошибок сценария произошла: assert.NoError(t, err, "конфиг должен парситься") выводит вашу подсказку после стандартного текста. Внутри обе проверки устроены тривиально — это сравнение с nil, — но именно они чаще всего оказываются в тестах: почти каждый вызов Go-функции возвращает error, и почти каждый тест начинается с проверки этого факта.

Пара NoError и Error в действии

GO
func TestRegister(t *testing.T) {
        err := Register("anna@example.com", 29)
        assert.NoError(t, err) // корректный вход: ошибки быть не должно

        err = Register("anna@example.com", -3)
        assert.Error(t, err) // отрицательный возраст обязан вернуть ошибку
}

Первый сценарий фиксирует счастливый путь, второй — защиту от плохого входа. При провале NoError в отчёт уйдёт текст ошибки от Register, а при провале Error сообщение честно скажет, что err оказался nil: функция приняла -3 и ничего не сказала.

assert.Nil против NoError: разница в отчёте#

assert.Nil(t, err) тоже проходит на nil-ошибке и тоже красит тест на настоящей ошибке — формально проверить ошибку им можно. Проблема вскрывается при провале: Nil печатает внутреннее представление значения, и в отчёте появляется что-то вроде Expected nil, but got: &errors.errorString{s:"connection refused"} — тип и содержимое структуры, из которых текст ошибки ещё надо выковырять глазами. NoError в той же ситуации выводит Received unexpected error: connection refused — сразу диагноз. Вторая причина — намерение: Nil говорит «значение пустое», NoError — «ошибки нет», и в тесте на ошибку вторая формулировка честнее описывает проверку. То же в другую сторону: для проверки, что err есть, Error читается яснее, чем NotNil. Грубое правило на каждый день: Nil и NotNil оставьте значениям — указателям, интерфейсам, слайсам, — а ошибки проверяйте парами Error и NoError.

EqualError: сверка текста целиком#

assert.EqualError(t, err, "expected text") — самая строгая проверка из семейства: она вызывает err.Error() и сравнивает результат со строкой дословно. Нюанс: err обязан быть не nil — на nil-ошибке проверка провалится с сообщением An error is expected but got nil. Рабочее место EqualError — там, где формулировка ошибки является частью контракта: валидация, которая обязана назвать проблемное значение; ошибка, чей текст попадает в ответ API; сообщение, по которому дежурный ищет инцидент в журналах. В этих случаях текст — не подробность, а обещание, и тест должен падать при любом его изменении. Внутри пакета, где формулировка — просто подсказка для человека, EqualError начинает мешать: каждое перефразирование сообщения красит тесты без изменения поведения. Там надёжнее проверять факт ошибки и её тип — об этом дальше.

EqualError на валидации

GO
func TestValidateAge(t *testing.T) {
        err := ValidateAge(-1)

        assert.EqualError(t, err, "age must be positive, got -1")
}

Сообщение валидации содержит значение аргумента — это часть обещания функции: человек в отчёте должен увидеть, какой возраст отклонён. Если кто-то уберёт значение из текста, тест упадёт: контракт нарушили, поведение изменилось, и EqualError фиксирует именно это.

ErrorIs и ErrorContains: sentinel и обёртки#

Go-код обычно не придумывает текст на месте, а отдаёт известную ошибку: sentinel объявляют переменной var ErrNotFound = errors.New("record not found") и возвращают из всех подходящих веток. Тест хочет сверить не текст, а факт: вернулась ли именно ErrNotFound. assert.ErrorIs(t, err, target) делает это через errors.Is — он пройдёт по цепочке обёрток и найдёт sentinel, даже если функция вернула fmt.Errorf("user %d: %w", id, ErrNotFound). Аргумент target — именно sentinel, а не строка. Для подстроки есть assert.ErrorContains(t, err, "substring"): он проверяет, что err не nil и текст ошибки содержит фрагмент. Это середина между строгим ErrorIs и дословным EqualError: подстрока устойчива к переформулировкам, но всё же проверяет содержимое. Типичный дуэт в тесте — ErrorIs по sentinel плюс ErrorContains по деталям вроде идентификатора записи.

Sentinel, обёртка и две проверки

GO
// package store
var ErrNotFound = errors.New("record not found")

func (s *Store) User(id int) (*User, error) {
        u, ok := s.users[id]
        if !ok {
                return nil, fmt.Errorf("user %d: %w", id, ErrNotFound)
        }
        return u, nil
}

func TestUserNotFound(t *testing.T) {
        s := NewStore()

        _, err := s.User(42)

        assert.ErrorIs(t, err, ErrNotFound)
        assert.ErrorContains(t, err, "user 42")
}

User оборачивает sentinel через %w, и в err лежит не сама ErrNotFound, а обёртка с контекстом. ErrorIs находит sentinel сквозь обёртку, ErrorContains сверяет подстроку с итоговым текстом — вместе они фиксируют и тип ошибки, и попадание id в сообщение. Прогнать этот тест можно только локальным go test: тестовые пакеты в браузерной песочнице не исполняются.

assert.ErrorIs не сверяет указатели и не сравнивает текст — он вызывает errors.Is и полагается на протокол обёрток. Ошибка может реализовать метод Unwrap() error и вернуть внутреннюю ошибку; errors.Is идёт по цепочке шаг за шагом, на каждом уровне сравнивая цель через == или метод Is(error) bool. Отсюда свойства, важные для тестов. Обёрток может быть сколько угодно: load config: read file: not found — три уровня, и sentinel на дне всё равно найдётся. С Go 1.20 протокол пополнился Unwrap() []error для ошибок, склеенных через errors.Join: ErrorIs находит sentinel в любой из ветвей. Работают и sentinel стандартной библиотеки: assert.ErrorIs(t, err, fs.ErrNotExist) — обычная проверка в тестах на файлы, sql.ErrNoRows — в тестах на хранилища. А assert.Equal про этот протокол не знает ничего: он сравнивает пару значений как есть, поэтому и ломается на первом же слое обёртки.

Как выбрать проверку ошибки#

  • Факт без деталей: assert.NoError на счастливом пути, assert.Error на сценарии с ошибкой.
  • Известный sentinel или ошибка стандартной библиотеки: assert.ErrorIs — он переживёт любую обёртку fmt.Errorf с %w.
  • Часть текста, которая обязана дойти до человека: assert.ErrorContains по подстроке.
  • Формулировка — контракт: assert.EqualError с дословной строкой.
  • Nil и NotNil — для значений, не для ошибок: пары NoError и Error дают внятный отчёт и честное намерение.

Пять проверок на одном экране#

ПроверкаЧто сверяетТипичное место
assert.NoErrorerr == nilсчастливый путь: функция отработала без ошибок
assert.Errorerr != nilплохой вход обязан вернуть ошибку
assert.EqualErrorerr.Error() и строка дословновалидация и ошибки-контракты с текстом
assert.ErrorContainsподстрока в тексте ошибкидетали вроде id в сообщении
assert.ErrorIserrors.Is по цепочке обёртокsentinel-ошибки пакета и stdlib

Проверка: сообщение при провале

Функция вернула ошибку connection refused. Какая проверка покажет её текст самым читаемым образом?

Проверка: обёрнутая ошибка

Функция вернула fmt.Errorf("load config: %w", ErrNotFound), где ErrNotFound = errors.New("not found"). Какая проверка пройдёт?

Что покажет прогон?

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

var ErrNotFound = errors.New("not found")

func load() error {
        return fmt.Errorf("read config: %w", ErrNotFound)
}

func TestLoad(t *testing.T) {
        err := load()

        assert.Equal(t, ErrNotFound, err)
        assert.ErrorIs(t, err, ErrNotFound)
}

Соберите проверки ошибок

Допишите проверки: счастливый путь, факт ошибки, сверка sentinel и подстрока с id.

func TestCreate(t *testing.T) { err := store.Create(item) require.(t, err) } func TestDelete(t *testing.T) { err := store.Delete(42) assert.(t, err) assert.ErrorIs(t, err, store.ErrNotFound) assert.(t, err, "42") }

Соберите тест на sentinel

Расставьте строки файла: объявление sentinel, тест, вызов, две проверки и закрывающая скобка.

Строки

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

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

Карточки: проверки ошибок

Термин

assert.NoError(t, err)

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

Проваливает тест, если err не nil; при провале печатает текст ошибки. Для ошибок яснее assert.Nil.

1 / 6

Что дальше#

Ошибки проверены — следующий вопрос: что сравнивать, когда функция вернула не ошибку, а структуру или слайс. Тема урока про сравнение структур и слайсов: там внутри ObjectsAreEqual видно, когда assert.Equal обходит поля через reflect.DeepEqual, а когда сравнивает байты. Для ошибок в подготовке действует правило прошлого урока про assert и require: require.NoError на ошибке запроса — предусловие, а проверки ошибок из этого урока — уже содержательные проверки результата. Примеры урока собираются в пакет с go test и запускаются локально: браузерная песочница не исполняет тестовые пакеты, зато go test на папке со store прогонит весь набор за секунды. Карта всего курса — на странице testify-курса.

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

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

Вывод

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

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

  • assert.NoError проверяет отсутствие ошибки, assert.Error — её наличие; при провале оба печатают текст ошибки.
  • assert.Nil тоже проходит на nil-ошибке, но при провале показывает внутреннее представление — NoError даёт внятный отчёт.
  • assert.EqualError сверяет err.Error() с точной строкой: уместно, когда формулировка — контракт.
  • assert.ErrorIs проверяет ошибку через errors.Is: sentinel находится сквозь обёртки fmt.Errorf с %w.
  • assert.ErrorContains ищет подстроку в тексте ошибки — середина между хрупким EqualError и строгим ErrorIs.
  • assert.Equal на ошибках ломается при любой обёртке: сравнение по значению не проходит по цепочке Unwrap.
  • Выбор проверки: факт — Error и NoError, sentinel — ErrorIs, подстрока — ErrorContains, точный текст — EqualError.

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

Добавьте в свой пакет sentinel-ошибку var ErrNotFound и оберните её при возврате через fmt.Errorf с %w. Перепишите проверки тестов на ErrorIs и ErrorContains, одну ошибку с фиксированным текстом проверьте EqualError. Прогоните go test локально и убедитесь, что assert.Equal на обёрнутой ошибке действительно красный.

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

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

Чем assert.NoError лучше assert.Nil для ошибок?

Обе проверки пройдут одинаково: nil-ошибка — это nil-интерфейс. Разница в отчёте при провале: Nil напечатает внутреннее представление значения, а NoError выведет текст самой ошибки. NoError ещё и честнее заявляет намерение: проверяется именно отсутствие ошибки.

Когда EqualError оправдан, если текст хрупкий?

Когда формулировка — часть контракта: текст попадает в ответ API, в алерт или содержит значение аргумента, которое должно дойти до человека. Внутри пакета, где текст — просто подробности, надёжнее ErrorIs по sentinel или ErrorContains по подстроке: рефакторинг формулировок не красит тесты.

ErrorIs работает с ошибками стандартной библиотеки?

Да: подходит любая sentinel-ошибка, которую цепочка обёрток сохранила. Типичные случаи — fs.ErrNotExist и sql.ErrNoRows после fmt.Errorf с %w: errors.Is их находит, значит и assert.ErrorIs зелёный. А Equal сравнивает значения напрямую и на обёртке провалится.

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

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

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

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

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

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

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