Как запустить все тесты в проекте Go и что происходит "под капотом"?
Каждый разработчик Go хотя бы раз в жизни набирал в терминале магическую команду go test ./... и наблюдал, как зелёные галочки бегут по экрану. Но задумывались ли вы, что на самом деле происходит в этот момент? Почему тесты запускаются именно так, а не иначе? И почему иногда go test молча пропускает ваши новенькие тесты?
В этой статье мы не просто ответим на популярный вопрос с Stack Overflow (где он собрал тысячи просмотров), а заглянем под капот Go-компилятора и узнаем, как устроен механизм тестирования. Поехали!
Почему go test ./... — это магия?
Давайте начнём с основ. Команда go test без аргументов запускает тесты только в текущем пакете. Но если у вас проект из десятков пакетов (а в реальном продакшне так и есть), вручную перебирать их — сродни средневековой пытке. Тут на помощь приходит ./....
Этот паттерн означает: «найди все пакеты рекурсивно, начиная с текущей директории». Go обходит дерево каталогов, игнорируя vendor и модули, и собирает список всех пакетов. Для каждого из них он запускает отдельный экземпляр go test.
Но что, если вы хотите запустить тесты только в определённых директориях? Например, только в ./internal/...? Работает так же: go test ./internal/....
Важно: если вы используете Go Modules (а с Go 1.16+ это стандарт), то ./... работает только в пределах текущего модуля. Чтобы протестировать все модули в workspace, придётся использовать go test ./... в каждом из них или настроить go.work.
А что такое «пакет» в контексте go test?
Go оперирует понятием пакет — группа .go файлов в одной директории. Тестовые файлы (с суффиксом _test.go) тоже считаются частью пакета, но компилируются отдельно. Когда вы запускаете go test, Go компилирует тестовый бинарник, который включает:
- Исходный код пакета (но только то, что используется в тестах)
- Код самих тестов
- Специальную рантайм-обвязку для запуска и отчёта
Этот бинарник запускается, выполняет тесты и выводит результат. Весь процесс — отдельный для каждого пакета.
Как Go решает, какие тесты запускать?
Когда вы пишете go test ./..., Go выполняет следующие шаги:
- Обход файловой системы — Go находит все директории, содержащие
.goфайлы. - Фильтрация — игнорируются директории
vendor,testdataи скрытые папки (начинающиеся с.). - Компиляция — для каждого пакета создаётся временный бинарник.
- Запуск — бинарник исполняется, результаты собираются.
- Кеширование — если код не менялся, Go использует кеш и показывает
(cached).
Последний пункт — частая причина удивления. Вы изменили тест, запускаете go test ./..., а он говорит «ok (cached)». Почему? Потому что Go кеширует результаты на основе хеша входных файлов. Если вы изменили только комментарий или неиспользуемый импорт, хеш не меняется, и тест не перезапускается.
Лайфхак: принудительно сбросить кеш можно командой go clean -testcache. Или используйте флаг -count=1, который заставляет Go игнорировать кеш: go test -count=1 ./....
Параллелизм: почему тесты не всегда быстрее?
По умолчанию go test запускает тесты в рамках одного пакета последовательно. Но между пакетами — параллельно! Количество одновременных пакетов определяется флагом -p (по умолчанию равен числу CPU).
Звучит здорово, но есть нюанс: если ваши тесты используют общие ресурсы (БД, файлы, порты), параллелизм может вызвать гонки данных и падения. В таких случаях используйте go test -p=1 ./..., чтобы запускать пакеты строго по очереди.
Пример из реального проекта: представьте, что у вас есть 10 пакетов, каждый из которых поднимает тестовый сервер на порту 8080. При параллельном запуске они упадут с ошибкой «address already in use». Решение — либо выделить каждому пакету свой порт (через конфиг), либо запускать последовательно.
Флаг -run: точечный удар
Иногда нужно запустить не все тесты, а только определённые. Для этого есть флаг -run, который принимает регулярное выражение:
go test -run "TestUser" ./...
Эта команда запустит только те тесты, в имени которых есть «TestUser». Обратите внимание: -run применяется к имени теста, а не к имени файла. Если у вас есть тесты TestUserCreate и TestUserDelete, оба будут запущены.
Для более тонкой фильтрации используйте -run "TestUser/Create" — это запустит только подтесты (subtests) с именем «Create» внутри TestUser.
Внутреннее устройство: что происходит в моменте?
Давайте заглянем в исходный код Go (да, он открыт!). Основная логика go test находится в пакете cmd/go. Когда вы вызываете go test ./..., происходит следующее:
- Сбор списка пакетов — через
go list ./.... Эта команда выводит все пакеты в JSON-формате. - Планирование — Go строит граф зависимостей между пакетами. Если пакет A зависит от пакета B, тесты B запустятся раньше? Нет! Тесты пакетов независимы, если только вы не используете флаг
-shuffleили не задали порядок вручную. - Компиляция — для каждого пакета создаётся временная директория (обычно в
$GOPATH/pkg/mod/cacheили$GOCACHE), куда складывается скомпилированный тестовый бинарник. - Запуск — бинарник исполняется, его stdout/stderr перехватываются и выводятся в терминал.
- С