Содержание урока · 11 разделов
После первых уроков курса в руках уже есть тяжёлая артиллерия: Equal для сравнения значений, ElementsMatch для коллекций, семейство проверок ошибок. Но повседневный тест — это не только сравнения. Проверить, что переключатель включился, что метод вернул nil вместо объекта, что очередь опустела, что счётчик обнулился — такие проверки занимают больше половины строк в тестах, которые вы будете писать каждую неделю. Testify собирает их в короткие одноимённые утверждения: True, False, Nil, NotNil, Empty, NotEmpty, Zero, NotZero, Len. Каждое работает одинаково: провал помечает тест через t.Errorf, в отчёт уходит и полученное значение, и ожидаемое, а вместе с ними — ваше сообщение, если вы его передали. В этом уроке пройдёмся по набору целиком, а затем остановимся на двух ловушках, на которых регулярно собираются редкие и жёсткие баги: nil, спрятанный в интерфейсе, и разница между пустым и нулевым значением коллекции.
True и False: флаги без развилок#
assert.True(t, ok) требует, чтобы ok был равен true, assert.False(t, ok) — чтобы false. На первый взгляд это дубль проверки ok == true через if, но разница проявляется при провале: if молча не выполнит ветку, а тест-функция с assert.True напишет в отчёт «Should be true» и покажет фактическое значение флага. Именно поэтому в тестах развилки if почти не живут: они проверяют вручную и молча, а тест обязан шуметь. Полезная деталь API: и True, и False принимают те же необязательные msgAndArgs, что и остальные утверждения — сообщение с контекстом («токен истёк за секунду до операции») попадёт в отчёт рядом с провалом. Частая ошибка новичка — assert.Equal(t, true, ok) вместо assert.True(t, ok): работает так же, но читается хуже и выводит более шумный diff. Инвертированные версии у каждой проверки тоже есть на месте: NotNil и NotEmpty — прямые антиподы Nil и Empty, и в паре с True/False они покрывают большинство сценариев с булевыми полями и результатами валидации.
Проверка флагов валидации
GOfunc TestValidation(t *testing.T) {
user := User{Name: "Анна", Age: 30}
assert.True(t, user.IsValid(), "заполненный пользователь должен проходить валидацию")
user.Age = -1
assert.False(t, user.IsValid(), "отрицательный возраст не должен проходить")
}Каждая проверка снабжена сообщением с человеческим объяснением: при провале отчёт покажет не только «expected false, got true», но и причину, ради которой проверка ставилась. Сообщения — дешёвая привычка, которая экономит часы при чтении отчётов CI.
assert.Nil: границы nil в Go#
assert.Nil(t, v) проверяет, что v — нулевое значение своего типа: nil для указателей, слайсов, карт, каналов, интерфейсов и функций, ноль для чисел, пустая строка для string, false для bool. Это шире, чем проверка v == nil: для числа 0 и строки "" Nil тоже сработает, потому что в терминах testify «нулевой» и «пустой» для примитивов означают одно и то же. Не путайте с NotNil: assert.NotNil(t, err) иногда встречается в чужих тестах вместо assert.Error, но так делать не стоит — для ошибок есть специальные проверки с более ясным выводом, они разобраны в уроке про проверку ошибок. Ещё одна привычка, которая отличает зрелый тест: проверять Nil не только на желаемые результаты, но и на то, что освобождение ресурсов обнулило указатель — так ловят использования после закрытия.
Классическая ловушка Go: переменная интерфейсного типа, в которую положили nil-указатель конкретного типа, не равна nil. err := (*MyError)(nil); var e error = err; assert.Nil(t, e) провалится, хотя по-человечески e выглядит как nil. Внутри интерфейса живут тип и значение, и здесь пара не пустая. Это ошибка проектирования, а не testify: лечится она в коде возвращаемых типов, а не в тесте — и о ней стоит помнить, когда Nil внезапно краснеет на «очевидном» nil.
Empty и NotEmpty: что считается пустым#
assert.Empty(t, v) отвечает на вопрос «здесь ничего полезного?» для самых разных типов: пустая строка, слайс длины ноль, пустая карта, nil того же семейства, нулевой канал, ноль числа, false. Утверждение построено на helper-функции, которая сначала пробует взять длину через отражение, а для примитивов сравнивает с нулевым значением. Отсюда практическое следствие: Empty объединяет в одной проверке то, что в чистом Go требует разных выражений — len(s) == 0, s == nil, s == "". NotEmpty — зеркальная проверка, и она особенно удобна для входных данных: assert.NotEmpty(t, resp.Items, "API обязан вернуть хотя бы один элемент") читается как требование продукта, а не как код.
Empty на четырёх типах
GOfunc TestEmpty(t *testing.T) {
var nilSlice []int
emptySlice := []int{}
filled := []int{1}
assert.Empty(t, nilSlice) // nil-слайс пуст
assert.Empty(t, emptySlice) // пустой слайс пуст
assert.Empty(t, "") // пустая строка пуста
assert.Empty(t, map[int]int{}) // пустая карта пуста
assert.NotEmpty(t, filled)
}И nil-слайс, и слайс с нулевой длиной проходят Empty — для testify оба означают «ничего нет». Если ваш тест обязан различать эти состояния, используйте Len из урока про коллекции или явный Equal с nil.
Что Empty считает пустым#
| Тип | Пустое значение | Комментарий |
|---|---|---|
| string | "" | и nil-семантики тут нет — только длина |
| слайс | nil или длина 0 | оба состояния эквивалентны |
| карта | nil или длина 0 | как и слайс |
| канал | nil | закрытый пустой канал тоже пройдёт Empty |
| число | 0 | 0, 0.0 — всё пустое |
| bool | false | false эквивалентен пустому |
| указатель | nil | а ненулевой указатель на пустую структуру — NotEmpty |
Zero и NotZero: нулевые значения#
Zero — родственник Empty, но с более строгой семантикой: он сравнивает значение с нулевым значением ТИПА, а не с абстрактной пустотой. assert.Zero(t, counter) после сброса счётчика читается точнее, чем Empty, потому что говорит о типовой нулевости. Для структур Zero проверяет рефлексией каждое поле — и здесь прячется нюанс: структура со всеми нулевыми полями пройдёт Zero, но структура с любым ненулевым полем провалит проверку с подробным diff. На практике Zero чаще всего встречается на числах и time.Duration, NotZero — на проверках, что что-то посчиталось: assert.NotZero(t, resp.Elapsed, "замер времени обязан быть ненулевым"). Для float-полей помните урок про сравнение чисел с плавающей точкой: Zero на аккумулированных долях лучше ставить только там, где ноль действительно точен.
Какое утверждение выбрать#
- Флаг или результат валидации — True и False, с сообщением о смысле проверки.
- Возможное отсутствие объекта — Nil и NotNil; для ошибок — Error и NoError, а не Nil.
- «Здесь нет данных» — Empty и NotEmpty: покрывают строки, слайсы, карты одним словом.
- Типовая нулевость после сброса — Zero и NotZero, особенно для чисел и Duration.
- Точная длина коллекции — Len: она различает nil и пустую коллекцию по контексту проверки.
Тест, который проверял NotEmpty на слайсе ответов, ловил багу сервиса, возвращавшего JSON null вместо пустого массива: клиентская библиотека превращала null в nil-слайс, NotEmpty честно проваливался, но контрактом это не лечилось. Разница «пусто, но существует» и «не существует вовсе» доходит до пользователей API: null и [] сериализуются по-разному. Решайте различие на уровне контракта и кодируйте его правильными утверждениями — Empty для «допустимо пусто», Len для «обязана быть конкретная длина», Equal с явным значением для контрактных деталей.
Чтение провала: что покажет отчёт#
Провал простой проверки информативен ровно настолько, насколько вы о нём позаботились. На Nil testify напишет ожидание nil и фактическое значение — если там спрятана структура, вывод будет многострочным. На Empty покажет тип и содержимое пустоты: строку, длину коллекции, ноль. На Zero выведет diff между нулевым значением типа и фактическим. Всё это можно и нужно снабжать сообщением: без него отчёт говорит «что», без «зачем», и через месяц после написания теста вы будете гадать, чем именно был важен этот Nil. Отдельный приём — проверять в сообщении условия, при которых проверка справедлива: «очередь пуста после Drain, но не до первого Run» — такое сообщение превращает провал в подсказку, а не в загадку.
Сообщение как документация проверки
GOassert.Nil(t, rows, "после Drain очередь обязана опустеть")
assert.NotEmpty(t, report.Rows,
"отчёт без строк после обработки — сигнал пропущенного шага")Оба сообщения объясняют требование, а не повторяют проверку. Первый читатель отчёта — вы сами через полгода: сообщение экономит погружение в контекст.
Проверка: Nil и интерфейсы
В тесте объявлено: var e error = (*MyError)(nil). Что вернёт assert.Nil(t, e)?
Проверка: Empty на nil и пустом
Какие проверки пройдут для var s []int (nil-слайс)?
Что выведет отчёт при провале?
Прочитайте код и выберите, что он выведет:
var users []*User // nil-слайс
assert.NotEmpty(t, users, "ожидаем пользователей")
assert.Zero(t, 0)
assert.Zero(t, "0")Соберите набор базовых проверок
Допишите утверждения: истинность флага, отсутствие объекта после освобождения, пустота карты, ненулевой замер времени.
Соберите тест жизненного цикла сессии
Расставьте строки теста: создание, проверка активности, освобождение, две проверки после.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: базовые проверки
Что дальше#
Базовый набор собран: флаги, nil, пустота, нулевые значения — теперь можно читать чужие тесты без словаря и писать свои так, чтобы отчёт рассказывал историю сам. Следующий шаг — сообщения: у каждого утверждения testify есть необязательный хвост msgAndArgs, и умение писать его коротко и по делу отличает тест, который приятно чинить, от теста, который просто краснеет. После сообщений перейдём к сборным конструкциям — сьютам с методами подготовки, где базовые проверки будут жить уже в организованных группах. Напомню про среду: все проверки выполняются внутри go test, поэтому полный прогон — локальный Go; в браузерной песочнице вы можете потренировать заполнение пропусков и порядок строк, а запуск тестов — уже на своей машине. Общий маршрут — на странице курса по testify, а если хочется освежить, откуда берутся нулевые значения в Go, — это урок про переменные и типы базового курса.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- True, False, Nil, NotNil, Empty, NotEmpty, Zero, NotZero — ежедневный набор проверок testify; каждая принимает msgAndArgs.
- Nil проверяет нулевое значение типа: не только nil, но и 0, "" и false — это шире, чем v == nil.
- Интерфейс с nil-указателем конкретного типа не равен nil — ловушка Go, сигнал чинить возвращаемый тип, а не тест.
- Empty объединяет «пустоту» разных типов: nil и длина 0 для слайсов и карт, пустая строка, ноль, false.
- Zero сравнивает с нулевым значением типа: строка "0" не нулевая — провалит проверку.
- Разницу «nil против пустого» на уровне контракта кодируйте Len или Equal — JSON null и [] не одно и то же.
- Для ошибок используйте Error и NoError: Nil на err работает, но вывод менее ясный.
Домашнее задание
Возьмите структуру с булевым полем и слайсом и напишите тест жизненного цикла: creation — True и NotEmpty, после Reset — Nil на указателе, Empty на слайсе, Zero на счётчике. К каждой проверке добавьте сообщение с объяснением смысла. Затем один раз намеренно перепутайте Empty и Zero на строковом поле и посмотрите, как отличает их вывод при провале.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем assert.Empty отличается от assert.Nil?
Nil — про нулевое значение конкретного значения: nil, 0, пустая строка, false. Empty — про отсутствие полезного содержимого: он проходит и для nil, и для коллекций нулевой длины. Практическое правило: Nil — для объектов и указателей, Empty — для данных внутри объектов и коллекций.
Почему assert.Nil провалился на переменной, в которой явно лежит nil?
Скорее всего, это интерфейсная ловушка Go: в переменную интерфейсного типа записан nil-указатель конкретного типа, и пара «тип-значение» внутри интерфейса не пустая. Чинить нужно код: возвращать ненулевой тип только вместе с ненулевым значением, а в тесте — не ослаблять проверку до assert.True(t, v == nil).
Стоит ли проверять err через assert.Nil?
Работать будет, но специальных проверок ошибок лучше: assert.NoError даёт более ясный вывод, а EqualError и ErrorIs позволяют сверить текст или саму ошибку. Nil на err оставьте там, где ошибки нет как концепции — например, в проверках обнуления ресурсов.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Проверка паник: Panics и PanicsWithValue
- msgAndArgs: свои сообщения в проверках
- Test suite: suite.Suite и его методы
- Хуки сьюта: SetupSuite, TearDownTest и компания
- InDelta и InEpsilon: числа с плавающей точкой
- ElementsMatch, Subset и Len: коллекции под контролем
- Сравнение структур и слайсов в тестах
- Проверка ошибок: Error, NoError и EqualError
- Все курсы по Go
Следующая ступень учебного пути
Viper: конфигурация приложений на Go
Интерактивный курс Viper для Go: YAML и env-переменные, приоритеты источников, Unmarshal, WatchConfig, профили окружений и секреты. 25 уроков с примерами.
- Собирать конфиг из файлов, окружения и флагов в один реестр
- Читать значения типизированно: Get, GetString, Unmarshal в структуры
- Задавать приоритеты источников: флаг > env > файл > default
- Перечитывать конфиг на лету через WatchConfig и OnConfigChange