сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Веб и тесты на Go

Тестирование в 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, остальные разбираются в тренажёре.

  1. #go_testing_basics1 / 5
    Чем 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, который тут же разыменуют). Другие тесты в любом случае продолжатся.

  2. #go_testing_basics2 / 5
    Как должен называться файл с тестами и куда он попадает при сборке?
    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).

  3. #go_testing_basics3 / 5
    Что делает t.Cleanup(f) в тесте?
    A)Откатывает изменения, сделанные тестом в глобальных переменных
    B)Удаляет временные файлы, созданные в каталоге пакета
    C)Очищает кэш результатов тестов перед следующим запуском
    D)Регистрирует функцию, которая выполнится по завершении теста
    показать ответ и разбор
    +D)Регистрирует функцию, которая выполнится по завершении теста

    // разбор: Аналог defer, но привязанный к тесту, а не к функции: зарегистрированное отработает и после подтестов, и при падении, и при t.Fatal. Удобнее defer тем, что вызов можно спрятать внутрь вспомогательной функции — она сама зарегистрирует уборку за собой, а тест останется коротким. Для временных каталогов есть готовый t.TempDir, он убирает за собой сам.

  4. #go_testing_basics4 / 5
    Зачем во вспомогательной функции теста вызывают t.Helper()?
    A)Чтобы отчёт указывал на строку вызова, а не на строку внутри хелпера
    B)Чтобы функция получила доступ к внутреннему состоянию пакета testing
    C)Чтобы падение внутри хелпера не прерывало весь тест целиком
    D)Чтобы хелпер выполнялся параллельно с телом основного теста
    показать ответ и разбор
    +A)Чтобы отчёт указывал на строку вызова, а не на строку внутри хелпера

    // разбор: Без этой пометки любой assertEqual сообщает об ошибке своей собственной строкой, и все падения в отчёте выглядят одинаково — искать приходится вручную. С t.Helper() рантайм пропускает кадр хелпера и показывает строку, откуда его позвали. Стоит одну строку в начале функции и сильно экономит время при разборе упавших прогонов.

  5. #go_testing_basics5 / 5
    Для чего в пакете объявляют функцию TestMain?
    A)Чтобы задать порядок выполнения тестов внутри пакета
    B)Чтобы выполнить общую подготовку и уборку вокруг всех тестов пакета
    C)Чтобы объявить точку входа, без которой тесты не запустятся
    D)Чтобы объединить несколько тестовых файлов в один прогон
    показать ответ и разбор
    +B)Чтобы выполнить общую подготовку и уборку вокруг всех тестов пакета

    // разбор: TestMain получает управление вместо тестов и сам решает, когда их запустить, вызвав m.Run(). Типичное применение — поднять контейнер с базой, накатить схему, прогнать тесты и прибрать за собой. Важная деталь: код возврата надо отдать через os.Exit(m.Run()) — а значит, уборку нельзя вешать на defer, он при таком выходе не сработает.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.