Тестирование в Go: пакет testing
Написал тест, который зовёт t.Fatal из горутины, и запустил. Горутина умерла, тест пометился как проваленный - и при этом тело теста спокойно доработало до конца, обе печати после Wait выполнились. Спрашивают ровно такие вещи: как называется файл, чем Fatal отличается от Error, что делают Cleanup и Helper. Сторонние библиотеки для тестов в Go не нужны.
Стержень: тест - обычная функция TestXxx(t *testing.T), а вокруг неё небольшой, но достаточный инструментарий.
// Формулировки: «как называется файл с тестами?», «Fatal или Error?», «зачем t.Helper?»
Файлы, имена, запуск
Файл обязан заканчиваться на _test.go - в обычную сборку он не попадёт, это правило самого тулчейна. Функция называется TestXxx и принимает *testing.T. Запускают через go test ./..., конкретный тест - флагом -run, и он понимает подтесты: -run 'TestSubtests/second' у меня запустил ровно один подтест из трёх.
Тест в том же пакете видит приватные детали. Вариант с суффиксом _test в имени пакета запирает тебя снаружи, и проверять приходится только публичные функции - ровно то, что видит настоящий потребитель библиотеки.
// Кэш результатов иногда путает. Второй прогон того же теста печатает ok chk/t3 (cached) и код не выполняет. Сбрасывается через go clean -testcache - я проверял, после него прогон снова настоящий.
- _test.go
- суффикс файла, который не попадает в сборку
- (cached)
- результат взят из кэша, код не выполнялся
Fatal, Error и горутины
t.Fatal помечает тест проваленным и немедленно прерывает его. t.Error помечает и продолжает - удобно, когда хочется увидеть все несоответствия за один прогон.
А вот из горутины Fatal ведёт себя иначе, и это я специально проверил запуском. Он завершает только ту горутину, в которой вызван: строка после него не выполнилась, тест получил отметку о провале, но тело теста продолжило работу и дошло до конца. То есть падение будет позже и не там, где ты его ждёшь.
// Вывод: в горутинах пользуются t.Error либо передают результат наружу через канал и разбирают его в теле теста, где Fatal работает как задумано.
- t.Fatal
- провал и остановка - но только своей горутины
- t.Error
- провал с продолжением: видно все несоответствия
Уборка и хелперы: измерено построчно
Зарегистрировал два t.Cleanup, поставил defer в теле теста и уронил тест через Fatal. Порядок вывода вышел такой: сначала defer, потом cleanup второй, потом cleanup первый. То есть уборка идёт стопкой, последний зарегистрированный первым, и отрабатывает даже после падения.
Чем Cleanup лучше defer: его вызов можно спрятать внутрь вспомогательной функции, и она сама зарегистрирует уборку за собой. Рядом живут t.TempDir - каталог, который убирается сам, и TestMain - общая подготовка на весь пакет.
// t.Helper проверил тем же способом. Без него отчёт показал строку 37 - это внутри вспомогательной функции, одинаково для всех падений. С ним показал строку 50 - место реального вызова. Одна строка в начале хелпера, а разбор падений становится вдвое быстрее.
- t.Cleanup
- уборка стопкой, работает и после падения
- t.Helper
- отчёт указывает на строку вызова, а не на хелпер
Как отвечать: «Чем t.Fatal отличается от t.Error и зачем нужен t.Helper?»
t.Fatal помечает тест проваленным и сразу прерывает его выполнение, t.Error помечает и даёт коду идти дальше. Fatal беру, когда дальше идти бессмысленно: не удалось подготовить данные, и все следующие проверки упадут каскадом, засоряя отчёт. Error беру, когда хочу увидеть все несоответствия за один прогон, например при сверке нескольких полей результата. Есть тонкость, которую я проверял руками: из горутины Fatal работает не так. Он завершает только ту горутину, где вызван - тест отметку о провале получает, но тело продолжает выполняться до конца, и падение всплывает не там, где ждёшь. Поэтому в горутинах пользуюсь Error или отдаю результат наружу через канал. t.Helper нужен вспомогательным функциям вроде assertEqual. Без него отчёт всегда показывает строку внутри хелпера: у меня это была строка 37 для всех падений подряд, и место реальной ошибки приходится искать глазами. С t.Helper показывается строка вызова, у меня 50. Стоит это одной строки в начале функции. И регулярно пользуюсь t.Cleanup: он удобнее defer тем, что уборку можно зарегистрировать прямо внутри хелпера, а отрабатывает она стопкой и даже после падения теста.
Сильный ответ: разница объяснена через критерий выбора, а не формально, названа неочевидная тонкость с горутинами и показано, что человек её проверял, польза Helper показана через конкретную боль в отчёте, и добавлен Cleanup с преимуществом, которого у defer нет.
На чём валят
- −Зовут t.Fatal из горутины и ждут, что тест остановится. Он остановит только эту горутину.
- −Не ставят t.Helper и ищут место падения по строке внутри хелпера.
- −Гонятся за процентом покрытия. Тест без единой проверки покрытие тоже даёт.
- −Забывают про кэш и разбирают зелёный прогон, который не выполнялся - в выводе стоит (cached).
- −Вешают уборку в TestMain на defer, а выходят через os.Exit: defer при этом не срабатывает.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 8, остальные разбираются в тренажёре.
- Чем t.Fatal отличается от t.Error в тесте?A)Fatal останавливает текущую тест-функцию сразу, Error помечает провал и продолжаетB)Fatal валит весь прогон тестов целиком, а Error портит только текущий файл с тестамиC)Разницы по сути нет, оба просто печатают сообщение в тестовый логD)Error обрывает тест, а Fatal лишь предупреждает и спокойно идёт дальше по коду
показать ответ и разбор
+A)Fatal останавливает текущую тест-функцию сразу, Error помечает провал и продолжает// разбор: t.Error/Errorf помечают тест проваленным, но выполнение функции ПРОДОЛЖАЕТСЯ — удобно собрать несколько несоответствий за прогон. t.Fatal/Fatalf помечают провал и НЕМЕДЛЕННО останавливают текущую тест-функцию (через runtime.Goexit) — берут, когда дальше проверять бессмысленно (например nil, который тут же разыменуют). Другие тесты в любом случае продолжатся.
- Как должен называться файл с тестами и куда он попадает при сборке?A)test_*.go; компилируется прямо в обычный прод-бинарник вместе с остальным кодомB)Файл с произвольным именем в каталоге tests/, подхватываемый по особому флагу сборкиC)Суффикс *_spec.go, и такой файл целиком исключается из модуля при сборке проектаD)*_test.go; в обычную сборку не входит, только в go test
показать ответ и разбор
+D)*_test.go; в обычную сборку не входит, только в go test// разбор: Файл с тестами обязан оканчиваться на _test.go. Такие файлы компилируются и запускаются ТОЛЬКО командой go test и не попадают в обычный go build — тесты не тянутся в прод-бинарник. Тест-файл может быть в том же пакете (белый ящик, доступ к приватному) или в пакете foo_test (чёрный ящик, только экспортируемое API).
- Что делает t.Cleanup(f) в тесте?A)Откатывает изменения, сделанные тестом в глобальных переменныхB)Удаляет временные файлы, созданные в каталоге пакетаC)Очищает кэш результатов тестов перед следующим запускомD)Регистрирует функцию, которая выполнится по завершении теста
показать ответ и разбор
+D)Регистрирует функцию, которая выполнится по завершении теста// разбор: Аналог defer, но привязанный к тесту, а не к функции: зарегистрированное отработает и после подтестов, и при падении, и при t.Fatal. Удобнее defer тем, что вызов можно спрятать внутрь вспомогательной функции — она сама зарегистрирует уборку за собой, а тест останется коротким. Для временных каталогов есть готовый t.TempDir, он убирает за собой сам.
- Зачем во вспомогательной функции теста вызывают t.Helper()?A)Чтобы отчёт указывал на строку вызова, а не на строку внутри хелпераB)Чтобы функция получила доступ к внутреннему состоянию пакета testingC)Чтобы падение внутри хелпера не прерывало весь тест целикомD)Чтобы хелпер выполнялся параллельно с телом основного теста
показать ответ и разбор
+A)Чтобы отчёт указывал на строку вызова, а не на строку внутри хелпера// разбор: Без этой пометки любой assertEqual сообщает об ошибке своей собственной строкой, и все падения в отчёте выглядят одинаково — искать приходится вручную. С t.Helper() рантайм пропускает кадр хелпера и показывает строку, откуда его позвали. Стоит одну строку в начале функции и сильно экономит время при разборе упавших прогонов.
- Для чего в пакете объявляют функцию TestMain?A)Чтобы задать порядок выполнения тестов внутри пакетаB)Чтобы выполнить общую подготовку и уборку вокруг всех тестов пакетаC)Чтобы объявить точку входа, без которой тесты не запустятсяD)Чтобы объединить несколько тестовых файлов в один прогон
показать ответ и разбор
+B)Чтобы выполнить общую подготовку и уборку вокруг всех тестов пакета// разбор: TestMain получает управление вместо тестов и сам решает, когда их запустить, вызвав m.Run(). Типичное применение — поднять контейнер с базой, накатить схему, прогнать тесты и прибрать за собой. Важная деталь: код возврата надо отдать через os.Exit(m.Run()) — а значит, уборку нельзя вешать на defer, он при таком выходе не сработает.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.