Содержание урока · 12 разделов
- Анатомия msgAndArgs: строка и аргументы
- Fail и FailNow: атомы проверок
- t.Helper(): сообщения из вспомогательных функций
- Когда сообщение лишнее
- Что писать в хвост
- Форматные глаголы, которые пригодятся в хвосте
- Сообщения в табличных тестах
- Правила хвоста msgAndArgs
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Вспомните сигнатуры проверок из прошлых уроков: assert.Equal(t, expected, actual) — а дальше? Дальше идёт вариативный хвост msgAndArgs: строка-сообщение и аргументы к ней. Если аргументы есть, testify прогоняет пару через fmt.Sprintf, и в отчёт уходит готовая строка; если только строка — она выводится как есть; если ничего — проверки работают как раньше. Механика минимальная, но именно она отделяет отчёты «expected 8080, actual 9090» от отчётов «порт из env перекрыл файл конфига: expected 8080, actual 9090». В этом уроке, продолжающем линию знакомства с testify, разберём правила хвоста, базовые Fail и FailNow, сообщения во вспомогательных функциях с t.Helper() и границу, за которой сообщение уже лишнее — потому что diff сам рассказывает историю.
Анатомия msgAndArgs: строка и аргументы#
Любая проверка testify выглядит как assert.Equal(t, expected, actual, msgAndArgs...), и хвост собирается по простым правилам. Первый элемент — сообщение: строка или форматная строка с глаголами %s и %d. Остальные элементы — аргументы для Sprintf: assert.Equal(t, got, want, "порт из окружения %q", envName) подставит имя переменной в отчёт. Передали строку без аргументов — она выводится без форматирования. Полезно знать и про особые значения: первый аргумент хвоста, будучи nil, игнорируется (историческая совместимость с assert.Conditions), но полагаться на это не стоит — в боевом коде хвост либо осмысленный, либо его нет. Аргументы формата экономят на лишних склейках: не нужно заранее собирать строку fmt.Sprintf, testify делает это только при провале — проверки с сообщениями не тратят форматирование на счастливом пути.
Сообщение как требование
GOfunc TestDrain(t *testing.T) {
q := NewQueue()
q.Push("a")
q.Push("b")
q.Drain()
assert.Empty(t, q, "после Drain очередь обязана опустеть")
assert.Equal(t, 0, q.Len(), "счётчик после Drain: ожидаю %d, счётчик живёт своей жизнью", 0)
}Первое сообщение объясняет требование, второе — намекает на гипотезу о причине (счётчик рассинхронизировался с содержимым). При провале оба попадут в отчёт раньше чтения кода очереди.
Fail и FailNow: атомы проверок#
Под каждой проверкой testify стоят две функции пакета testing, и testify открывает их через assert.Fail(t, failureMessage, msgAndArgs...) и require.FailNow. Fail помечает тест проваленным и продолжает выполнение — как assert с несбывшимся условием, только без сравнения вовсе: вы сами решаете, что провал случился. Это удобно для недостижимых веток: default в разборе типа, который вы считаете полным, — assert.Fail(t, "неожиданный тип события: %T", evt) фиксирует, что мир изменился. FailNow из require-пакета делает то же, но немедленно останавливает тест: правило из урока про assert и require в чистом виде — подготовка не удалась, продолжать бессмысленно. Дымовые тесты «объект собирается без паники» собираются на связке require.NoError и Fail: меньше условий — меньше мест, где тест врёт.
t.Helper(): сообщения из вспомогательных функций#
Как только проверка заворачивается во вспомогательную функцию — assertSessionActive(t, s) — отчёт начинает врать адресами: строка провала указывает на helper, а не на тест. Приём стандартной библиотеки: в начале helper вызовите t.Helper(), и тест-раннер будет пропускать эту функцию в стеке провалов. Внутри helper сообщение работает по тем же правилам, но его роль выше: helper переиспользуется в десятках тестов, и сообщение — единственное место, где написано, что именно он гарантирует. Комбинация из t.Helper() и хвоста msgAndArgs делает свои ассерты почти полноценными: assertSessionActive(t, s) при провале покажет и требование, и фактическое состояние сессии — без дублирования кода по файлу. Побочный бонус: Require-версия helper обязана вызываться из потока теста, о чём напомнит паника FailNow, если helper начнут звать из горутин.
Вспомогательная проверка с Helper
GOfunc assertSessionActive(t *testing.T, s *Session) {
t.Helper()
require.NotNil(t, s, "сессия должна существовать к этому моменту")
require.False(t, s.Expired(), "сессия не может быть истекшей: проверка через %s", s.ID)
assert.True(t, s.IsActive, "активность потеряна: %v", s)
}t.Helper() убирает функцию из стека провала: строка отчёта укажет на тест, который вызвал helper. Require-проверки для подготовки — сессия обязана существовать, иначе продолжать бессмысленно; assert — для самих инвариантов.
Когда сообщение лишнее#
Хвост — не обязанность, а инструмент, и у него есть обратная сторона. Equal на простых числах уже показывает «expected 8080, actual 9090» — сообщение «порты не равны» добавляет шум, а не смысл. Правило экономное: сообщение ставится там, где провал не объясняется diff-ом — инварианты порядка, условия после Prepare, проверки контрактов, гипотезы о причине. На цепочке проверок одного объекта сообщение обычно несёт первое звено (подготовка), дальше diff говорит сам. Отдельный случай — сообщения, дублирующие имя теста: TestServerPort_Defaults already says everything, и хвост «проверяем порт по умолчанию» пересказывает заголовок. Экономия — не скупость: отчёты с десятью повторными строками читают реже, чем отчёты с одной точной подсказкой в нужном месте. Хорошее сообщение — требование продукта или гипотеза о причине («заморозка тарифа за 3 дня до списания ломает расчёт»); плохое — пересказ диффа («actual is not equal to expected»), который и так выведен, — его лучше опустить вместе с хвостом.
Что писать в хвост#
| Ситуация | Сообщение | Почему |
|---|---|---|
| Инвариант после операции | «после Drain очередь обязана опустеть» | diff покажет содержимое, сообщение — требование |
| Проверка с гипотезой | «счётчик живёт своей жизнью: %d» | подсказка, куда смотреть при провале |
| Helper-проверка | требование, которое helper гарантирует | одно место на десятки вызовов |
| Diff и так понятен | без сообщения | Equal уже показал expected и actual |
| Пересказ имени теста | не писать | шум в отчёте |
Форматные глаголы, которые пригодятся в хвосте#
Раз аргументы прогоняются через fmt.Sprintf, в сообщениях работает весь арсенал глаголов Go, и пара из них создана как будто для тестовых отчётов. %q печатает строку в кавычках — идеально для имён переменных окружения и конфигурационных ключей, где важно видеть границы. %T выводит тип значения — незаменим в сообщениях о неожиданных типах: "неожиданный тип события: %T". %v остаётся универсальным принтером структур: в хвосте он полезен в конце сообщения, когда хотите показать весь объект — но с осторожностью: большие структуры раздуют отчёт, и diff Equal уже покажет их красивее. %d для счётчиков, %s для текстов — обычная работа. Практическое правило по объёму: одна-две подстановки в сообщении, иначе сообщение превращается в дамп, который читают реже, чем сам diff.
Глаголы в боевых сообщениях
GOassert.Equal(t, 9090, port, "ключ %q из окружения, тип %T", key, port)
assert.NotEmpty(t, cfg.DBHost, "хост БД обязателен: окружение %s, файл %s", env, cfgPath)
assert.Fail(t, "неожиданный тип события: %T", evt)Три типовых сообщения: %q с типом в первом, пара строк-источников во втором, %T для типа события в третьем. Каждое подсказывает, где искать причину, ещё до чтения diff.
Сообщения в табличных тестах#
В табличных тестах (слайс кейсов, цикл, t.Run — им будет посвящён отдельный урок дальше по курсу) хвост msgAndArgs получает ещё одну роль: идентификация кейса. Слайс кейсов крутится в цикле, и провал внутри t.Run показывает имя подтеста — но если кейсы короткие, полезно продублировать суть в сообщении: assert.Equal(t, tc.want, got, "кейс %q: ожидаем %d", tc.name, tc.want). Такой хвост объединяет в отчёте имя кейса, ожидание и факт — не нужно прыгать к объявлению структуры кейса. Обратная сторона — дублирование: если имя кейса уже показывается в заголовке t.Run, повторять его в сообщении стоит только тогда, когда само сообщение добавляет смысл — ожидаемое значение, источник, гипотезу. Баланс тот же, что и везде: сообщение отвечает на «что проверить и почему», заголовок — на «какой это кейс».
Файл на двести строк проверок без единого хвоста — не признак чистоты, а долг: при провале вы получаете список фактов без контекста и читаете тест целиком, чтобы понять, чей инвариант сломался. Спор внутри команд нормальный: у одной стороны «сообщения у каждой проверки» — за предсказуемые отчёты, у другой «только у нетривиальных» — за чистоту кода. Граница разумного: сообщения обязательны у инвариантов и helper-ов, у простых Equal — по желанию автора.
Правила хвоста msgAndArgs#
- Сообщение — требование или гипотеза о причине, а не пересказ diff и не повтор имени теста.
- Аргументы формата подставляются через fmt.Sprintf только при провале — на счастливом пути бесплатно.
- В helper-функциях t.Helper() обязателен: иначе стек провала указывает на helper, а не на тест.
- Fail — осознанный провал без сравнения, FailNow — провал с остановкой; оба принимают тот же хвост.
- Простые Equal без контекста живут без сообщений: diff говорит сам.
Проверка: форматирование хвоста
Как testify обработает хвост "порт из окружения %q", envName при провале проверки?
Проверка: helper и стек провала
Вспомогательная функция checkInvariant(t, x) без вызова t.Helper() провалила assert внутри себя. Куда укажет строка провала в отчёте?
Что попадёт в отчёт?
Прочитайте код и выберите, что он выведет:
q := NewQueue()
q.Push("a")
q.Drain()
assert.Empty(t, q)Соберите проверки с сообщениями
Допишите функцию провала, вызов helper-пометки и форматный хвост.
Соберите тест с сообщениями
Расставьте строки: подготовка через require, инвариант через assert, хвосты у обеих.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: сообщения в проверках
Что дальше#
Хвост msgAndArgs разложен: строка и аргументы через Sprintf, Fail и FailNow как атомы провала, t.Helper() для своих ассертов. Дальше объём тестов начинает расти, и их надо организовывать: сьюты из пакета suite собирают группу тестов одного модуля со структурой-состоянием и методами подготовки. Там сообщения из этого урока встретятся с хуками — и окажется, что требование «после Drain очередь пуста» удобнее всего держать именно в хелпере сьют-структуры. Напомню про среду: сообщения видны в выводе go test на локальном Go — песочница здесь не заменит прогон, но потренировать пропуски и порядок строк можно прямо на странице. Общий маршрут — на странице курса по testify, а про форматирование строк в Go — в уроке про fmt базового курса.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Каждая проверка testify принимает хвост msgAndArgs: строка-сообщение и аргументы для fmt.Sprintf.
- Форматирование хвоста происходит только при провале — сообщения не стоят ничего на счастливом пути.
- Сообщение — требование или гипотеза о причине; пересказ diff и повтор имени теста — шум.
- assert.Fail — осознанный провал без сравнения (недостижимые ветки), require.FailNow — провал с остановкой.
- t.Helper() в вспомогательной проверке перенаправляет стек провала на тест — обязателен в своих ассертах.
- Простые Equal без контекста живут без сообщений: diff уже показывает expected и actual.
- Сообщения обязательны у инвариантов и helper-ов — они дешевле чтения всего теста при провале.
Домашнее задание
Возьмите любой тест из прошлых уроков и пройдите по проверкам: у инвариантов и helper-ов добавьте хвосты-требования, у простых Equal — уберите пересказы. Затем заверните три проверки в функцию с t.Helper() и намеренно провалите её: сравните строку отчёта с и без Helper и запишите разницу.
Личные заметкиПусто
Вопросы и ответы по уроку
Нужно ли сообщение у каждой проверки?
Нет. Правило экономное: обязательны сообщения у инвариантов, контрактов и helper-ов, где провал не объясняется diff-ом. Простые Equal с понятными expected и actual читаются и без хвоста. Спор «у каждой» против «у нетривиальных» — вопрос стиля команды, а не правильности.
Чем assert.Fail отличается от require.FailNow?
Fail помечает тест проваленным и продолжает выполнение — полезно, когда хотите увидеть все проблемы сразу. FailNow останавливает тест немедленно через t.FailNow: продолжать бессмысленно, когда не сработала подготовка. Оба принимают тот же хвост msgAndArgs.
Зачем t.Helper(), если функция и так маленькая?
Helper нужен не для размера, а для адреса провала: без него отчёт указывает на строку внутри вспомогательной функции, и при переиспользовании из десяти тестов вы не поймёте, какой вызов сломался. С t.Helper() провал привязывается к строке теста, где функцию вызвали.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Test suite: suite.Suite и его методы
- Хуки сьюта: SetupSuite, TearDownTest и компания
- Табличные тесты с testify
- Знакомство с mock.Mock: первый мок
- Проверка паник: Panics и PanicsWithValue
- True, False, Nil, NotNil и Empty: простые проверки
- InDelta и InEpsilon: числа с плавающей точкой
- ElementsMatch, Subset и Len: коллекции под контролем
- Все курсы по Go
Следующая ступень учебного пути
Viper: конфигурация приложений на Go
Интерактивный курс Viper для Go: YAML и env-переменные, приоритеты источников, Unmarshal, WatchConfig, профили окружений и секреты. 25 уроков с примерами.
- Собирать конфиг из файлов, окружения и флагов в один реестр
- Читать значения типизированно: Get, GetString, Unmarshal в структуры
- Задавать приоритеты источников: флаг > env > файл > default
- Перечитывать конфиг на лету через WatchConfig и OnConfigChange