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 і все одно отримувати кеш, якщо решта команди «кешована».
Зробімо маленьку таблицю, щоб було простіше втримати в голові:
| Прапорець | Про що | Впливає на кеш? |
|---|---|---|
|
детальний вивід | так, входить до cacheable flags |
|
вибір тестів за regexp | так, входить до cacheable flags |
|
не стартувати нові після першого падіння | так, входить до cacheable flags |
|
скільки разів прогнати | залежить від значення; -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, бо це ідіоматичний спосіб вимкнути кеш.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ