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)
}

Тест із табличним підходом і підтестами:

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-режимі ви побачите, які тести реально стартували, включно з підтестами. Це особливо корисно, якщо тестів багато й ви хочете швидко переконатися, що запускається саме те, що ви очікуєте.

3. Прапорець -run: запуск лише потрібних тестів

Іноді хочеться не «прогнати все», а швидко перевірити одну конкретну річ. Наприклад, ви лагодите один тест, і вам не потрібно чекати, поки пробіжать 300 інших. Для цього є -run.

Офіційний опис каже: -run regexp запускає лише ті тести, приклади й fuzz-тести, які збігаються з регулярним виразом.

Як -run працює з підтестами (t.Run)

Найкорисніше те, що -run розуміє імена підтестів. У документації прямо сказано: для тестів регулярний вираз розбивається за символом / на послідовність регулярних виразів, і кожна частина ідентифікатора тесту має збігтися зі своїм регулярним виразом.

Тобто тест TestNormalizeTitle_Table/trim можна прицілити так:

go test -v -run TestNormalizeTitle_Table/trim ./...

І ось важливий нюанс: у документації сказано, що також запускаються «батьківські» збіги. Тому -run=X/Y може запускати всі тести, що збіглися з X, навіть якщо в них немає підтестів, що збіглися з Y, — адже їх потрібно запустити, щоб узагалі «пошукати» ці підтести.

Тобто -run TestNormalizeTitle_Table/trim гарантує, що TestNormalizeTitle_Table запуститься, а вже всередині нього система відфільтрує потрібні підтести.

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("неочікуваний результат: %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. Також там є важлива деталь: приклади завжди запускаються один раз, а -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 прицілюється в конкретний підтест, -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/підтест реально стартував. У verbose-режимі go test логує тести в міру виконання. Тому під час діагностики -run майже завжди «дружить» із -v.

Помилка №3: дивуватися, що -run X/Y запускає більше, ніж один підтест.
У go test регулярний вираз для тестів розбивається за /, і при цьому «батьківські» тести все одно запускаються, щоб знайти підтести. Тому -run=X/Y може запускати всі тести, що збіглися з X, навіть якщо в якихось із них немає підтестів, що збіглися з Y. Це не баг — це логіка пошуку по дереву тестів.

Помилка №4: не відрізняти «зупинити все» від «не починати нові» у -failfast.
-failfast не звучить як kill switch. Він каже: після першого падіння не стартувати нові тести. Якщо у вас уже запущені якісь тести або пакетні прогони йдуть у своєму порядку, ви все одно можете побачити частину виводу після першої помилки. Це нормально.

Помилка №5: «я виправив баг, але результат той самий» — і забути про -count=1.
Якщо ви запускаєте тести в package list mode, успішні результати можуть братися з кеша, і ви побачите (cached) замість часу. У момент «полагодив / не полагодив» дуже корисно явно поставити -count=1, бо це ідіоматичний спосіб вимкнути кеш.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ