Перейти до змісту

Пакет testing

Тестування вбудоване у toolchain. Не треба встановлювати фреймворк, не потрібна бібліотека для assertions, і не треба налаштовувати раннер — файл із суфіксом _test.go і функція, що починається з Test, вже є тестом.

func TestNormalize(t *testing.T) {
    got, err := Normalize("  Hello  ")
    if err != nil {
        t.Fatalf("Normalize() unexpected error: %v", err)
    }
    if got != "hello" {
        t.Errorf("Normalize() = %q, want %q", got, "hello")
    }
}

Правила

  • Ім'я файлу закінчується на _test.go. Такі файли виключені зі звичайних збірок, тож тестовий код ніколи не потрапляє в продакшн.
  • Функція має вигляд func TestXxx(t *testing.T). Ім'я після Test має починатися з великої літери — Testthing тестом не є, і ніхто вас про це не попередить.
  • Тестові файли лежать поруч із кодом, який вони тестують, у тому самому пакеті. Окремого дерева tests/ немає.

Запускайте їх через go test ./... з будь-якого місця в модулі.

Ніяких assertions, лише if

Go не постачає assertEqual. Ви порівнюєте через if і повідомляєте через методи *testing.T:

Метод Ефект
t.Errorf зафіксувати провал, продовжити виконання
t.Fatalf зафіксувати провал і зупинити цей тест
t.Logf занотувати щось; показується лише з -v або при провалі
t.Skipf пропустити, з причиною

Вибір між Errorf і Fatalf — це питання того, чи має сенс продовжувати. Якщо наступний рядок призведе до паніки — nil-результат, провалене налаштування — використовуйте Fatalf. Інакше Errorf повідомляє про кілька проблем за один прогін, замість того щоб змушувати вас виправляти їх по одній.

t.Fatalf викликає runtime.Goexit, тож зупиняє лише ту горутину (goroutine), в якій виконується. Виклик цього методу з горутини, яку запустив ваш тест, не завершує тест належним чином — замість цього надсилайте провал назад у горутину тесту.

Пишіть повідомлення про провал для того, хто його читатиме

Тест, що провалюється з повідомленням false, не каже нічого. Прийнято виводити, що ви викликали, що отримали і що очікували:

t.Errorf("Sum(%v) = %d, want %d", xs, got, want)
    calc_test.go:44: Sum([1 2]) = 3, want 4
--- FAIL: TestFailureMessage (0.00s)

Файл і рядок додаються автоматично. Використовуйте %q для рядків, щоб відмінності в пробілах були видимі — саме тому "hello " і "hello" інакше нерозрізнимі у виводі.

Запуск підмножини

go test ./...                    # everything
go test ./internal/store/...     # one package tree
go test -v ./...                 # show each test
go test -run TestNormalize ./... # regex match on the name
go test -count=1 ./...           # defeat the result cache

-run приймає регулярний вираз, а не буквальний рядок, тож -run TestNormalize також збігається з TestNormalizeTable. Прив'яжіть його якорями через -run '^TestNormalize$', коли це важливо.

Результати кешуються: незмінений пакет виводить (cached) і не перезапускається. Зазвичай це саме те, що потрібно, а -count=1 — спосіб примусити реальний запуск.

Внутрішні та зовнішні тестові пакети

Тестовий файл може оголосити один із двох пакетів:

package store        // internal: sees unexported identifiers
package store_test   // external: only the public API

store_test — єдиний виняток із правила "один пакет на директорію", і компілятор дозволяє обидва варіанти в одній папці.

Типово використовуйте внутрішню форму. Звертайтесь до store_test, коли хочете переконатися, що експортоване API придатне до використання саме собою, або щоб розірвати цикл імпорту — коли тесту потрібен пакет, що імпортує той, який тестується.

Налаштування і прибирання

t.Cleanup реєструє роботу, яка виконається, коли тест завершиться, включно з провалом. Виконується в порядку "останній зареєстрований — перший виконаний", як defer, але переживає виклики допоміжних функцій:

func TestThing(t *testing.T) {
    srv := startServer()
    t.Cleanup(srv.Close)
    // ...
}

Надавайте йому перевагу над defer у тестах: хелпер може зареєструвати власне прибирання, чого defer усередині цього хелпера зробити не міг би.

TestMain дає пакету одноразове налаштування і зобов'язаний викликати m.Run та завершитися з його кодом:

func TestMain(m *testing.M) {
    // setup
    code := m.Run()
    // teardown
    os.Exit(code)
}

Зверніть увагу на os.Exit — через нього відкладені функції в TestMain не виконуються, тож розміщуйте прибирання перед виходом.

Покриття, бенчмарки й детектор гонитв

go test -cover ./...
# ok   scratch   0.399s   coverage: 44.4% of statements

go test -race ./...

-race інструментує бінарний файл, щоб виявляти одночасний доступ до однієї й тієї самої пам'яті. Це повільніше, і повідомляє лише про ті гонитви, що дійсно сталися під час прогону — але знаходить справжні баги, яких жодне читання коду не знайде. Запускайте його в CI.

Швидкий бенчмарк, розглянутий детально в статті бенчмарки, фазинг і детектор гонитв:

go test -run XXX -bench . -benchmem ./...
# BenchmarkSum-12   3177772   370.7 ns/op   0 B/op   0 allocs/op

-run XXX не збігається з жодним тестом, тож запускаються лише бенчмарки.

Функції-приклади

Функція з іменем ExampleXxx та коментарем // Output: компілюється, виконується, і її вивід порівнюється:

func ExampleNormalize() {
    s, _ := Normalize("  Go  ")
    fmt.Println(s)
    // Output: go
}

Такі функції з'являються в go doc та на pkg.go.dev, тож вони є документацією, яка не може застаріти. Приберіть коментар // Output: — і функція компілюватиметься, але не виконуватиметься.

З досвіду Python: найближчий аналог — pytest, тільки без магії. Немає фікстур, немає переписування assert, немає плагінів, немає conftest.py. Ви пишете звичайні порівняння й звичайні повідомлення про помилки, що вимагає більше набору тексту, але значно легше читати, коли щось ламається о третій ночі.

Швидка довідка

Задача Форма
тест func TestXxx(t *testing.T) у *_test.go
провалити й продовжити t.Errorf("got %v, want %v", got, want)
провалити й зупинити t.Fatalf(...)
прибирання t.Cleanup(fn) — краще за defer у тестах
налаштування на рівні пакета TestMain(m *testing.M), обов'язково os.Exit(m.Run())
тести лише публічного API package foo_test
запустити все go test ./...
детальний вивід / фільтрація -v, -run '^TestX$'
примусити реальний запуск -count=1
покриття / гонитви -cover, -race
виконувана документація ExampleXxx з // Output:

Джерела