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

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

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

Урок 10 из 25 ~16 мин +20 XP

msgAndArgs: свои сообщения в проверках

Провал проверки отвечает на вопрос «что не совпало», но не на вопрос «почему это важно». Через месяц после написания теста второй вопрос дороже первого: читать diff легко, вспоминать, зачем была проверка, — нет. У каждой проверки testify есть необязательный хвост msgAndArgs: сообщение с аргументами, которое попадает в отчёт рядом с провалом. Умение писать этот хвост коротко и по делу отличает тест, который чинят за минуту, от теста, который просто красный.

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

Вспомните сигнатуры проверок из прошлых уроков: 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 делает это только при провале — проверки с сообщениями не тратят форматирование на счастливом пути.

Сообщение как требование

GO
func 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

GO
func 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.

Глаголы в боевых сообщениях

GO
assert.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, повторять его в сообщении стоит только тогда, когда само сообщение добавляет смысл — ожидаемое значение, источник, гипотезу. Баланс тот же, что и везде: сообщение отвечает на «что проверить и почему», заголовок — на «какой это кейс».

Правила хвоста 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-пометки и форматный хвост.

assert.(t, "неожиданный тип события: %T", evt) func assertSessionActive(t *testing.T, s *Session) { t.() require.NotNil(t, s, "сессия должна существовать") assert.True(t, s.IsActive, "активность потеряна: %s", s.) }

Соберите тест с сообщениями

Расставьте строки: подготовка через require, инвариант через assert, хвосты у обеих.

Строки

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

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

Карточки: сообщения в проверках

Термин

msgAndArgs

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

Необязательный хвост любой проверки testify: строка-сообщение и аргументы для fmt.Sprintf. Форматирование — только при провале.

1 / 6

Что дальше#

Хвост 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 и запишите разницу.

Личные заметкиПусто
Хранятся только в вашем браузере (Testify · msgAndArgs: свои сообщения в проверках)

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

Нужно ли сообщение у каждой проверки?

Нет. Правило экономное: обязательны сообщения у инвариантов, контрактов и helper-ов, где провал не объясняется diff-ом. Простые Equal с понятными expected и actual читаются и без хвоста. Спор «у каждой» против «у нетривиальных» — вопрос стиля команды, а не правильности.

Чем assert.Fail отличается от require.FailNow?

Fail помечает тест проваленным и продолжает выполнение — полезно, когда хотите увидеть все проблемы сразу. FailNow останавливает тест немедленно через t.FailNow: продолжать бессмысленно, когда не сработала подготовка. Оба принимают тот же хвост msgAndArgs.

Зачем t.Helper(), если функция и так маленькая?

Helper нужен не для размера, а для адреса провала: без него отчёт указывает на строку внутри вспомогательной функции, и при переиспользовании из десяти тестов вы не поймёте, какой вызов сломался. С t.Helper() провал привязывается к строке теста, где функцию вызвали.

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

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

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

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

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

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

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