JavaRush /Курсы /Go SELF /go test: -run, -count, -v, -failfast и кеш

go test: -run, -count, -v, -failfast и кеш

Go SELF
31 уровень , 0 лекция
Открыта

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 и всё равно получать кеш, если остальная команда «кешируемая».

Сделаем маленькую таблицу, чтобы было проще удержать в голове:

Флаг Про что Влияет на кеш?
-v
подробный вывод да, входит в cacheable flags
-run
выбор тестов по regexp да, входит в cacheable flags
-failfast
не стартовать новые после первого падения да, входит в cacheable flags
-count
сколько раз прогнать зависит от значения; -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, потому что это идиоматический способ отключить кеш.

1
Задача
Go SELF, 31 уровень, 0 лекция
Недоступна
Логи теста
Логи теста
1
Задача
Go SELF, 31 уровень, 0 лекция
Недоступна
Один subtest
Один subtest
1
Задача
Go SELF, 31 уровень, 0 лекция
Недоступна
Флаги запуска
Флаги запуска
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ