1. Что делает go test и почему флаги важны
Когда вы только начали писать тесты, кажется, что жизнь проста: написал go test, получил зелёное “PASS”, пошёл пить чай. Но как только тестов становится больше десяти, а падение происходит «где-то в середине», начинается типичная студенческая медитация: «почему оно упало… и где именно… и почему опять…».
Флаги go test — это ваш способ управлять запуском: запускать не всё подряд, а ровно нужное; видеть больше деталей; останавливать прогон быстрее; а ещё понимать, почему тесты иногда «выполняются мгновенно» (спойлер: это может быть кеш). В официальном описании go test прямо сказано, что он компилирует пакет и *_test.go, а затем запускает отдельный тестовый бинарник для пакета.
Два режима запуска: go test и go test . — не одно и то же
Сюрприз номер один (и он полезный): go test работает в двух режимах.
В “local directory mode” вы запускаете go test без аргументов пакета (например, просто go test или go test -v) — и в этом режиме кеширование отключено.
Во втором режиме, “package list mode”, вы явно указываете пакет(ы): go test ., go test ./..., go test math. И вот тут go test может кешировать успешные результаты, чтобы не гонять одно и то же снова и снова.
Чтобы это не выглядело магией, представим в виде схемы:
flowchart TD
A["Вы запускаете go test"] --> B{"Указаны пакеты? (go test . / ./...)"}
B -- "нет (go test)" --> C["Local directory mode: кеша нет"]
B -- "да" --> D["Package list mode: кеш возможен"]
C --> E["Собрали тестовый бинарник Запустили Показали итог"]
D --> F{"Есть подходящий результат в кеше?"}
F -- "да" --> G["Показываем прошлый вывод вместо запуска (видно '(cached)')"]
F -- "нет" --> E
2. Флаг -v: подробный вывод
Когда тесты падают, новичок обычно хочет две вещи: понять, какой тест упал, и увидеть контекст. По умолчанию go test часто экономит вывод и показывает много деталей только при падении пакета. Но есть режим, где он становится разговорчивым: -v.
В справке go test сказано, что -v включает verbose output: логирует тесты по мере выполнения и печатает текст из Log/Logf, даже если тест успешен. Также важно, что в package list mode go test печатает полный вывод даже для успешных пакетов, если вы указали -v (или -bench).
Мини‑пример: маленькая функция и тесты
Представим, что в учебном проекте есть пакет todo, и мы хотим нормализовать заголовки задач (чтобы не хранить " купить хлеб " как есть).
package todo
import "strings"
// NormalizeTitle приводит заголовок к аккуратному виду.
func NormalizeTitle(s string) string {
return strings.TrimSpace(s)
}
Тест с table‑driven и subtests:
package todo
import "testing"
func TestNormalizeTitle_Table(t *testing.T) {
cases := []struct {
name, in, want string
}{
{name: "trim", in: " buy milk ", want: "buy milk"},
{name: "empty", in: " ", want: ""},
}
for _, tc := range cases {
tc := tc // чтобы замыкание t.Run не схватило "плавающую" переменную
t.Run(tc.name, func(t *testing.T) {
if got := NormalizeTitle(tc.in); got != tc.want {
t.Fatalf("NormalizeTitle(%q)=%q, want %q", tc.in, got, tc.want)
}
})
}
}
Теперь команда:
go test -v ./...
В verbose‑режиме вы увидите, какие тесты реально стартовали, включая subtests. Это особенно полезно, если тестов много и вы хотите быстро убедиться, что запускается именно то, что вы ожидаете.
3. Флаг -run: запуск только нужных тестов
Иногда хочется не «прогнать всё», а быстро проверить одну конкретную вещь. Например, вы чините один тест, и вам не нужно ждать, пока пробегут 300 других. Для этого есть -run.
Официальное описание говорит: -run regexp запускает только те tests/examples/fuzz tests, которые матчятся регулярным выражением.
Как -run работает с subtests (t.Run)
Самое полезное — то, что -run понимает имена subtests. В документации прямо сказано: для тестов регулярка разбивается по символу / на последовательность регулярных выражений, и каждая часть идентификатора теста должна совпасть со своей регуляркой.
То есть тест TestNormalizeTitle_Table/trim можно прицелить так:
go test -v -run TestNormalizeTitle_Table/trim ./...
И вот важный нюанс: в документации сказано, что возможные родители совпадений тоже запускаются, поэтому -run=X/Y может запускать все тесты, совпадающие с X, даже если у них нет subtest’ов, совпадающих с Y — потому что их нужно запустить, чтобы вообще «поискать» эти subtest’ы.
То есть -run TestNormalizeTitle_Table/trim гарантирует, что TestNormalizeTitle_Table стартанёт, а уже внутри него система отфильтрует нужные subtests.
4. Флаг -failfast: упало — не стартуем новые тесты
Когда тестов много, один падёж часто тянет за собой лавину: один тест сломал состояние, и дальше падают остальные; или просто много независимых падений, а вам нужно увидеть первое, чтобы начать с него.
-failfast делает именно это: “Do not start new tests after the first test failure.” Обратите внимание на формулировку: он не обещает мгновенно остановить уже запущенное; он говорит «не начинать новые». Для новичка это правильная ментальная модель: как только что-то упало, прогон «старается аккуратно свернуться» и не разгонять ещё больше тестов.
Мини‑пример
Представим, что вы временно сломали ожидание:
t.Run("trim", func(t *testing.T) {
if got := NormalizeTitle(" buy milk "); got != "BUY MILK" {
t.Fatalf("unexpected result: %q", got)
}
})
Если вы запускаете так:
go test -failfast -v ./...
то после первого падения запуск не будет продолжать стартовать новые тесты (где это возможно).
5. Кеширование тестов: почему иногда “всё прошло за 0.00s”
Теперь к самой мистической части: вы запускаете go test ./..., и он отрабатывает быстро. Потом вы запускаете второй раз — и он отрабатывает подозрительно быстро. И вы начинаете подозревать либо заговор, либо что Go «слишком умный».
На самом деле go test действительно кеширует успешные результаты, но только в package list mode. В документации сказано: “In package list mode only, go test caches successful package test results…”, а если результат берётся из кеша, go test переотображает предыдущий вывод вместо повторного запуска бинарника и печатает (cached) вместо времени.
Что должно совпасть, чтобы кеш сработал
У кеша есть правила. В документации сказано: совпасть должен тот же тестовый бинарник, а флаги должны быть из ограниченного набора “cacheable flags”. В список входят, среди прочего, -failfast, -run и -v.
Это хороший момент для понимания: вы можете прицельно запускать тесты с -run и всё равно получать кеш, если остальная команда «кешируемая».
Сделаем маленькую таблицу, чтобы было проще удержать в голове:
| Флаг | Про что | Влияет на кеш? |
|---|---|---|
|
подробный вывод | да, входит в cacheable flags |
|
выбор тестов по regexp | да, входит в cacheable flags |
|
не стартовать новые после первого падения | да, входит в cacheable flags |
|
сколько раз прогнать | зависит от значения; -count=1 используют, чтобы отключать кеш |
(Таблица здесь именно для «быстро запомнить», а не чтобы выучить наизусть.)
6. Флаг -count: повторный прогон и честный перезапуск без кеша
Когда вы слышите слово “count”, можно подумать «ну, это сколько тестов». На самом деле это «сколько раз прогнать». В документации сказано: -count n запускает каждый тест (и бенчмарк, и fuzz seed) n раз; значение по умолчанию 1. Также там есть важная деталь: examples всегда запускаются один раз, а -count не применяется к fuzz‑тестам, выбранным через -fuzz.
Почему -count=1 вообще нужен, если “1” — это и так default
Потому что -count=1 используют как идиоматический способ отключить кеширование тестов. Это не догадка — это прямо прописано в описании кеша: “The idiomatic way to disable test caching explicitly is to use -count=1.”
Да, звучит странно: “count=1” как бы «ничего не меняет», но на уровне механики запуска он заставляет go test выполнить тестовый бинарник заново, а не брать прошлый результат из кеша.
Это особенно важно в ситуациях, когда тест зависит от внешнего мира: времени, окружения, файлов, базы, сети (хотя сеть в unit‑тестах лучше не трогать, но жизнь полна сюрпризов). И даже если вы уверены, что пишете «чистые» тесты, -count=1 — хороший способ быстро ответить себе на вопрос: «это реально сейчас выполнено или просто показали прошлое?»
Мини‑режим отладки: мой любимый набор
Когда вы чините один тест, типичная комбинация выглядит так:
go test -v -run TestNormalizeTitle_Table/trim -count=1 ./...
Здесь -run целится в конкретный subtest, -v даёт подробный вывод, а -count=1 гарантирует, что вы видите результат этого прогона, а не «переигранный» вывод из кеша.
Мини‑стратегия чтения вывода, чтобы не утонуть
Когда тесты падают, очень хочется сразу ковырять код. Но чаще всего быстрее сначала убедиться, что вы запускаете нужное и видите нужное.
Я обычно мысленно делаю так: сначала включаю -v, чтобы увидеть список запусков. Затем, если падение локализовано, подключаю -run, чтобы не гонять лишнее. И если результат «слишком быстрый и подозрительный» — добавляю -count=1, чтобы убрать кеш.
Если же «падает всё подряд», иногда выгодно включить -failfast, чтобы начать с первой причины и не смотреть на 50 вторичных симптомов.
7. Типичные ошибки
Ошибка №1: думать, что кеш работает “всегда”, и не понимать, почему он то есть, то нет.
Очень частая путаница возникает из-за двух режимов go test. Если вы запускаете go test без аргументов пакета, это local directory mode, и кеширование там отключено. А если вы запускаете go test . или go test ./..., это package list mode, и там кеш возможен. Если вы не различаете эти режимы, поведение кажется случайным — хотя оно довольно строгое.
Ошибка №2: использовать -run, но не включать -v, и потом гадать “а он вообще запускал то, что я просил?”.
-run фильтрует тесты, но без verbose‑вывода вам трудно быстро понять, какой именно TestXxx/subtest реально стартовал. В verbose‑режиме go test логирует тесты по мере выполнения. Поэтому при диагностике -run почти всегда дружит с -v.
Ошибка №3: удивляться, что -run X/Y запускает больше, чем один subtest.
В go test регулярка для тестов разбивается по /, и при этом «родительские» тесты всё равно запускаются, чтобы найти subtests. Поэтому -run=X/Y может запускать все тесты, совпавшие с X, даже если у каких‑то из них нет subtests, совпадающих с Y. Это не баг — это логика поиска по дереву тестов.
Ошибка №4: не отличать “остановить всё” от “не начинать новые” в -failfast.
-failfast не звучит как “kill switch”. Он говорит: после первого падения не стартовать новые тесты. Если у вас уже запущены какие-то тесты (или пакетные прогоны идут своим порядком), вы всё равно можете увидеть часть вывода после первой ошибки. Это нормально.
Ошибка №5: “я исправил баг, но результат тот же” — и забыть про -count=1.
Если вы запускаете тесты в package list mode, успешные результаты могут браться из кеша, и вы увидите (cached) вместо времени. В момент «починил/не починил» очень полезно явно сделать -count=1, потому что это идиоматический способ отключить кеш.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ