Библиотека установлена в прошлом уроке и пока не сделала ничего: сам по себе импорт в тест-файле не запускает ни одной проверки. Соберём теперь настоящий тест целиком — от пустого файла до отчёта о провале — и пойдём не по гладкой дорожке: второй прогон будет красным, и в этом главный план урока. Провал testify-теста — не просто строка FAIL: отчёт показывает ожидаемое значение, фактическое, diff между ними и адрес строки, где проверка не сошлась. Кто умеет читать этот отчёт, тот чинит регрессии заметно быстрее того, кто перечитывает логи. По пути разберём анатомию тестовой функции, флаг -v, статусы go test и самую частую ошибку новичка — перепутанные местами expected и actual. Полный прогон, напомню, требует локального Go: браузерная песочница не исполняет пакеты тестирования, поэтому запускаем всё в терминале своей машины.
Анатомия первого теста: TestXxx(t *testing.T)#
Тест в Go — обычная функция с тремя условиями. Первое: файл с суффиксом _test.go, иначе go test его не соберёт — суффикс, не префикс, и это частая опечатка у пришедших из других языков. Второе: имя начинается с Test и продолжается заглавной буквой — TestParseUser, TestCalcSum; компилятор равнодушен к testparseuser, но runner такой функции не найдёт. Третье: единственный аргумент t testing.T — через него testify и весь reporting общаются с пакетом testing: все assert-функции принимают t первым аргументом и отмечают провалы именно на нём. Забытый t не скомпилируется, лишний параметр — тоже. Пакет у теста тот же, что у тестируемого кода: Split лежит в calc.go пакета calc, тест — в calc_test.go того же пакета, и непубличные идентификаторы видны напрямую. Альтернативный пакет calc_test с внешним API пригодится позже, в теме заглушек. Сигнатура фиксирована: func TestXxx(t testing.T), ничего не возвращает — на этом магия заканчивается, дальше обычный код.
Тест, который должен пройти
GO// calc_test.go
package calc
import (
"testing"
"github.com/stretchr/testify/assert"
)
// Split делит строку по разделителю и возвращает число частей
func TestSplit(t *testing.T) {
got := Split("a,b,c", ",")
assert.Equal(t, 3, got)
}Файл в пакете calc, функция с Test и t *testing.T, внутри один assert.Equal: ожидаемое 3 вторым аргументом, фактическое got третьим. Комментарий над Split — не украшение: тест должен читаться отдельно от кода. В реальном проекте Split живёт в calc.go, а тест — в calc_test.go рядом; локальный прогон go test ./... покажет зелёное ok calc.
Запуск: go test и флаг -v#
Команда go test запускается в каталоге пакета, а go test ./... — по всем пакетам модуля разом. По умолчанию вывод скромен до аскетизма: ok calc или FAIL calc — и всё, имена прошедших тестов не печатаются. Флаг -v (verbose) разворачивает историю: каждый тест получает строку === RUN с именем и завершение PASS или FAIL с временем, и по журналу видно, какие проверки вообще выполнялись. Второй полезный флаг — -run: go test -run TestSplit гоняет только совпавшие по имени тесты, и на большом пакете экономит минуты. Третья деталь, которая сбивает с толку: go test кэширует результаты — неизменившийся пакет отвечает ok с пометкой (cached) и по-настоящему не перезапускается; свежий прогон задаётся флагом -count=1. Как и весь курс, полный прогон требует локального Go — браузерная песочница не исполняет пакеты тестирования, поэтому здесь терминал заменяют интерактивы: квизы, сборка отчёта по строкам, чтение вывода по частям.
Нарочный провал и его отчёт
GOfunc TestSplit(t *testing.T) {
got := Split("a,b", ",")
assert.Equal(t, 3, got)
}
// go test -v
// === RUN TestSplit
// calc_test.go:11:
// Error Trace: calc_test.go:11
// Error: Not equal:
// expected: 3
// actual : 2
// Test: TestSplit
// --- FAIL: TestSplit (0.00s)
// FAIL
// FAIL calc 0.002sРазница с рабочим тестом — одна буква: Split получает a,b вместо a,b,c и возвращает 2. Тест падает, и testify печатает отчёт: адрес строки, Not equal, ожидаемое 3, фактическое 2, имя теста. Падение локализовано: --- FAIL относится к TestSplit, а не ко всему пакету — остальные тесты файла выполняются как ни в чём не бывало.
Читаем отчёт о провале#
Отчёт читается сверху вниз по четырём слоям. Слой запуска: === RUN TestSplit — тест стартовал. Слой деталей: строка с адресом файла и строки проверки, затем Error: Not equal и два значения — expected, то, что вы передали вторым аргументом, и actual, то, что пришло из кода; testify печатает ещё строку Test с именем — удобно, когда журнал длинный. Слой вердикта: --- FAIL: TestSplit (0.00s) с временем. Слой итога: FAIL пакета и суммарное время; команда возвращает код выхода 1 — CI краснеет именно из-за него, а не из-за текста. Заметьте тонкость: testify пишет слова «ожидаемое» и «фактическое», но не знает, кто из них прав в вашей предметной области, — библиотека лишь честно показывает, что вы передали. Кто есть кто, решаете вы, и здесь критичен порядок аргументов: о классической ошибке с перепутанными значениями — ниже.
Классическая ошибка первого теста: assert.Equal(t, got, want) — значения поменяны местами. Тест упадёт так же, отчёт останется читаемым, но diff перевёрнут: «ожидаемым» станет то, что реально пришло из кода, и баг при поиске уводит в обратном направлении. Дисциплина простая: вторым аргументом — константа или эталон, третьим — результат вызова. Читая чужие провалы, сверяйте diff с кодом строки из Error Trace.
Что происходит при нескольких провалах в одном тесте. Проверки assert не прерывают функцию: первый упавший Equal лишь вносит строку в отчёт, и выполнение идёт дальше. Следствие — за один прогон видны все расхождения сразу: если из пяти проверок не сошлись три, отчёт покажет три блока expected/actual подряд, а не первый. Это экономит цикл «исправил — перезапустил — нашёл следующее», который на пакете из сотен тестов ощутимо дорог. Оборотная сторона: вторая проверка выполняется на данных, скомпрометированных первым провалом. Если результат парсинга оказался nil, следующие проверки не добавят информацию, а упадут с паникой и завалят отчёт стеком горутины — поэтому подготовку данных ведут через require, и это тема следующего урока. Паника в тесте тоже даёт FAIL, но выглядит иначе: вертикальный стек вызовов вместо аккуратного expected/actual. Увидеть такой отчёт хотя бы раз стоит — чтобы потом отличать мгновенно.
Два провала в одном тесте
GOfunc TestUser(t *testing.T) {
user := ParseUser("ann", 17)
assert.Equal(t, "ann", user.Name)
assert.Equal(t, 18, user.Age)
assert.Equal(t, "ann@example.com", user.Email)
}Возраст 17 против ожидаемого 18 и пустой Email против ann@example.com — два провала; имя сойдётся. Отчёт покажет два блока Error по очереди, с адресами строк. Если ParseUser на плохом входе возвращает nil, обращение к полю упадёт с паникой — тот случай, когда нужна ранняя остановка require, и мы её разберём в третьем уроке.
Мини-ритуал первого теста#
- Файл с суффиксом _test.go в пакете тестируемого кода
- Функция TestXxx с единственным аргументом t *testing.T
- assert.Equal(t, эталон, результат) — ожидаемое вторым, фактическое третьим
- Подготовка данных — через require, чтобы тест останавливался на битом входе
- Запуск go test -v в каталоге пакета, go test ./... — по всему модулю
- Красный тест — не авария, а данные: читайте expected, actual и Error Trace
Статусы и строки go test#
| Что видите | Что это значит | Что делать |
|---|---|---|
| ok calc | Все тесты пакета прошли | Ничего; с -v покажет имена и PASS по каждому |
| --- FAIL: TestSplit | Упал конкретный тест | Читать блок Error: expected, actual и адрес строки |
| FAIL calc | В пакете есть провалы или паника | Искать первый --- FAIL выше по выводу |
| [no test files] | go test не нашёл файлов _test.go | Проверить суффикс файла и каталог |
| ok calc 0.002s (cached) | Кэш: код не менялся, прогон честный, но старый | Свежий запуск — go test -count=1 |
Проверка: порядок аргументов
Как правильно вызвать проверку равенства в testify?
Проверка: флаг -v
Что добавляет go test -v по сравнению с простым go test?
Что напечатает прогон?
Прочитайте код и выберите, что он выведет:
// Abs возвращает модуль числа
func TestAbs(t *testing.T) {
got := Abs(-7)
assert.Equal(t, 7, got)
}Допишите проверку
Восстановите тип t, имя пакета проверки и поле структуры.
Соберите отчёт о провале
Расставьте строки вывода go test -v по порядку — от запуска теста до итога пакета (вывод сокращён).
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: первый тест
Не спешите чинить провал до чтения отчёта: expected и actual с адресом строки часто отвечают на вопрос целиком. Полезная привычка при отладке чужого бага — нарочно написать assert с гипотезой: если ожидание не сойдётся, отчёт покажет фактическое значение из глубины кода, и принт-отладка не понадобится.
Спорное: один assert на тест#
В TDD-мире живёт правило «один assert на тест»: тест должен падать по одной причине, и имя честно описывает провал. На практике testify оно ломается о первый же тест объекта: пять полей — пять вызовов Equal, и пять тестов с одинаковым парсингом ради каждого поля выглядят натужно. Моя позиция: несколько assert-ов в одном тесте стоит избегать не механически, а по границе — пока проверки относятся к одному сценарию одного объекта, они имеют право жить рядом; как только второй assert проверяет другой сценарий, тест делится. Компромисс, который работает на ревью: пачка Equal по полям одного результата — нормально, а assert на «и парсинг, и запись в базу, и ответ» — нет. Строгая школа права в одном: провалы внутри одного теста связаны, и require-версии с ранней остановкой дисциплинируют тут лучше любых правил.
Что дальше#
Первый тест собран, сломан и прочитан — дальше встаёт вопрос о судьбе провала: когда тест должен падать сразу, а когда полезно увидеть все расхождения. Это граница assert и require, и она размечена в следующем уроке курса. После require логичный шаг — проверки ошибок: Error, NoError и EqualError вместо if err != nil, тема четвёртого урока. Примеры урока прогоняются локальным Go: соберите тест из блока выше, сломайте значение нарочно и прочитайте свой первый diff — рука запоминает этот отчёт быстрее, чем глаза. Оглавление и маршрут курса, если хотите опережать расписание, — на странице курса testify.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Тест — функция TestXxx(t *testing.T) в файле с суффиксом _test.go: без заглавной буквы после Test и без t runner тест не увидит.
- assert.Equal(t, want, got): t первым, эталон вторым, результат кода третьим — при провале testify печатает expected, actual и адрес строки.
- go test -v показывает === RUN и PASS/FAIL по каждому тесту; -run сужает прогон, -count=1 обходит кэш результатов.
- Отчёт провала читается слоями: RUN, блок Error с адресом строки и значениями, --- FAIL с именем, FAIL пакета и код выхода 1.
- Перепутанные expected и actual не ломают тест, но переворачивают diff: эталон — вторым аргументом, результат — третьим.
- Несколько assert в одном тесте падают вместе: за прогон видны все расхождения; битая подготовка данных — сигнал для require.
- Красный тест — данные: assert с гипотезой и его отчёт часто заменяют принт-отладку.
Домашнее задание
Напишите тест к любой своей функции с assert.Equal и запустите go test -v — убедитесь в PASS. Затем нарочно поменяйте местами эталон и результат, перезапустите и сравните два отчёта: запомните, как выглядит перевёрнутый diff, чтобы узнавать его в чужих провалах.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему тест с ошибкой в имени не запускается, а компиляция проходит?
go test ищет функции по правилу: имя начинается с Test и продолжается заглавной буквой, аргумент t *testing.T. Всё остальное — обычные функции пакета. Компилятору такие функции корректны, поэтому ошибка видна только в выводе: «no tests to run» и отсутствие строки === RUN.
Что делать, если go test показывает ok (cached), а я менял код?
Кэш сбрасывается изменением файла или переменных окружения; если результат всё равно старый, запустите go test -count=1 — он игнорирует кэш и выполняет тесты заново. Флаг -v к кэшу отношения не имеет: он управляет подробностью вывода, а не свежестью прогона.
Можно ли запускать эти тесты в браузерной песочнице сайта?
Полный прогон go test — нет: песочница исполняет чистый Go без пакетов тестирования и модулей. Интерактивы урока — квизы, predict, fill и order — закрывают чтение кода и отчётов, а запуск и красный diff вы получаете локальным Go за пару минут.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Viper: конфигурация приложений на Go
Интерактивный курс Viper для Go: YAML и env-переменные, приоритеты источников, Unmarshal, WatchConfig, профили окружений и секреты. 25 уроков с примерами.
- Собирать конфиг из файлов, окружения и флагов в один реестр
- Читать значения типизированно: Get, GetString, Unmarshal в структуры
- Задавать приоритеты источников: флаг > env > файл > default
- Перечитывать конфиг на лету через WatchConfig и OnConfigChange