Тестируемый код
Инструменты тестирования Go входят в стандартный тулчейн: пакет testing и команда go test. Ничего устанавливать не нужно, библиотека утверждений не требуется.
Примеры на этой странице тестируют одну маленькую функцию, Slugify, которая превращает заголовок в slug для URL. Вот она, готовая к запуску, с самодельной проверкой в main:
Редактор в браузере запускает main; go test он запустить не может. Всё, что ниже, записано так, как это выглядит в настоящем проекте, а после каждой команды показан вывод терминала. Всё это запускалось на Go 1.24.
Первый тест
В проекте функция лежит в пакете, а её тесты рядом, в файле, имя которого заканчивается на _test.go:
textutil/
├── go.mod module example.com/textutil
├── slug.go package textutil, func Slugify
└── slug_test.go package textutil, the tests
// slug_test.go
package textutil
import "testing"
func TestSlugify(t *testing.T) {
got := Slugify("Hello, World!")
want := "hello-world"
if got != want {
t.Errorf("Slugify(%q) = %q, want %q", "Hello, World!", got, want)
}
}
Правила, которыми пользуется go test:
- Файлами тестов считаются только файлы с окончанием
_test.go.go buildих игнорирует, поэтому тестовый код никогда не попадает в ваш бинарник. - Тест это функция с именем
TestXxx(часть послеTestне должна начинаться со строчной буквы), принимающая один*testing.T. - Тест проходит, если не вызвал ни одного метода провала и не запаниковал.
go test # the package in the current directory
go test ./... # every package in the module
$ go test
PASS
ok example.com/textutil 0.318s
Формат сообщения о провале Func(input) = got, want expected это соглашение Go. Указывайте в сообщении входные данные: провал, где написано только got "a", want "b", заставит вас лезть в код, чтобы понять, какой это был ввод.
t.Errorf и t.Fatalf
| Метод | Помечает провал | Останавливает тест |
|---|---|---|
t.Error, t.Errorf | да | нет, выполнение продолжается |
t.Fatal, t.Fatalf | да | да, немедленно |
t.Log, t.Logf | нет | нет, печатает только с -v или при провале |
t.Skip, t.Skipf | нет | да, отмечается как пропущенный |
По умолчанию используйте Errorf, чтобы один запуск сообщил о каждом неверном поле. Fatalf используйте, когда остаток теста не может работать, обычно после неожиданной ошибки:
func TestCountWords(t *testing.T) {
path := writeFile(t, "doc.txt", "the quick brown\nfox jumps\n")
got, err := CountWords(path)
if err != nil {
t.Fatalf("CountWords: unexpected error: %v", err) // stop: got is meaningless
}
if got != 5 {
t.Errorf("CountWords = %d, want 5", got)
}
}
Fatal останавливает тест вызовом runtime.Goexit, поэтому его нужно вызывать из собственной горутины теста. Из запущенной вами горутины сообщайте через t.Error и возвращайтесь.
Табличные тесты с t.Run
Большинство тестов на Go табличные: слайс случаев, один цикл и t.Run, чтобы дать каждому случаю своё имя.
func TestSlugifyTable(t *testing.T) {
tests := []struct {
name, in, want string
}{
{"simple", "Go Testing", "go-testing"},
{"punctuation", "What's new in Go 1.24?", "what-s-new-in-go-1-24"},
{"extra spaces", " lots of space ", "lots-of-space"},
{"non-ascii dropped", "Café au lait", "caf-au-lait"},
{"empty", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Slugify(tt.in); got != tt.want {
t.Errorf("Slugify(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}
Новый случай добавляется одной строкой. О каждом подтесте сообщается отдельно, Fatal внутри одного завершает только этот подтест, а запустить можно один подтест по имени. Запустите всё с -v:
$ go test -v
=== RUN TestSlugify
--- PASS: TestSlugify (0.00s)
=== RUN TestSlugifyTable
=== RUN TestSlugifyTable/simple
=== RUN TestSlugifyTable/punctuation
=== RUN TestSlugifyTable/extra_spaces
=== RUN TestSlugifyTable/non-ascii_dropped
=== RUN TestSlugifyTable/empty
--- PASS: TestSlugifyTable (0.00s)
--- PASS: TestSlugifyTable/simple (0.00s)
--- PASS: TestSlugifyTable/punctuation (0.00s)
--- PASS: TestSlugifyTable/extra_spaces (0.00s)
--- PASS: TestSlugifyTable/non-ascii_dropped (0.00s)
--- PASS: TestSlugifyTable/empty (0.00s)
=== RUN TestCountWords
--- PASS: TestCountWords (0.00s)
=== RUN TestCountWordsMissingFile
--- PASS: TestCountWordsMissingFile (0.00s)
=== RUN ExampleSlugify
--- PASS: ExampleSlugify (0.00s)
PASS
ok example.com/textutil 0.182s
Пробелы в именах подтестов превращаются в подчёркивания. Когда случай проваливается, вывод называет подтест и строку:
--- FAIL: TestSlugifyTable (0.00s)
--- FAIL: TestSlugifyTable/underscore (0.00s)
slug_test.go:27: Slugify("snake_case_name") = "snake-case-name", want "snake_case_name"
FAIL
FAIL example.com/textutil 0.289s
Начиная с Go 1.22 у каждой итерации цикла свой tt, поэтому замыкания внутри t.Run видят правильный случай, даже когда подтесты выполняются параллельно. В старом коде по этой причине в начале тела цикла часто стоит tt := tt; теперь это не нужно.
Чтобы подтесты выполнялись параллельно, вызовите t.Parallel() в начале функции подтеста. Делайте так, только когда случаи независимы и достаточно медленные, чтобы это имело значение.
Флаги go test, которыми вы будете пользоваться
| Команда | Что делает |
|---|---|
go test -v | печатает имя, результат и вывод t.Log каждого теста |
go test -run TestSlugify | запускает тесты, имена которых подходят под регулярное выражение |
go test -run 'TestSlugifyTable/empty' | запускает один подтест |
go test -count=1 | игнорирует кешированные результаты и запускает заново |
go test -race | запускает с детектором гонок данных |
go test -short | просит долгие тесты пропустить себя (if testing.Short() { t.Skip() }) |
go test -failfast | останавливается после первого проваленного теста |
go test -timeout 30s | проваливает запуск, если он длится дольше (по умолчанию 10 минут) |
go test -cover | печатает покрытие инструкций |
go test -bench=. | запускает ещё и бенчмарки |
Когда вы указываете пакеты (go test ., go test ./...), go test кеширует результаты для пакетов, код и входные данные которых не менялись, и печатает после них (cached). Просто go test без аргументов никогда не кеширует. Кеш отслеживает файлы и переменные окружения, которые тест читает через пакет os, но не внешнее состояние вроде базы данных или сетевого сервиса, так что тест, который от них зависит, может пройти из кеша, хотя сейчас бы провалился; -count=1 заставляет выполнить настоящий запуск.
Помощники, временные папки и очистка
Вызов writeFile в TestCountWords выше это тестовый помощник:
// writeFile is a test helper: t.Helper makes failures point at the caller.
func writeFile(t *testing.T, name, content string) string {
t.Helper()
path := filepath.Join(t.TempDir(), name) // removed automatically after the test
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatalf("writing %s: %v", name, err)
}
return path
}
t.Helper()помечает функцию как помощник, поэтому о провале сообщается на строке теста, которая её вызвала, а не внутри помощника.t.TempDir()создаёт новую папку и удаляет её, когда тест заканчивается. Тестам никогда не нужно самим убирать файлы.t.Cleanup(func() { ... })регистрирует любую другую очистку (закрыть сервер, удалить таблицу). Функции очистки выполняются после теста и его подтестов, последняя зарегистрированная первой.t.Setenv("KEY", "value")задаёт переменную окружения только для этого теста и потом восстанавливает её.t.Context()(Go 1.24) возвращает контекст, который отменяется прямо перед выполнением функций очистки.
Файлы с тестовыми данными кладутся в папку testdata рядом с тестами. Утилита go не считает её пакетом, а тесты запускаются с папкой пакета в качестве рабочей, так что os.ReadFile("testdata/input.json") работает.
Тестирование ошибок и HTTP-обработчиков
Проверяйте ошибки через errors.Is или errors.As, так же как это делал бы вызывающий код:
func TestCountWordsMissingFile(t *testing.T) {
_, err := CountWords(filepath.Join(t.TempDir(), "nope.txt"))
if !errors.Is(err, fs.ErrNotExist) {
t.Errorf("err = %v, want fs.ErrNotExist", err)
}
}
Сравнение строк ошибок ломается, как только кто-то переформулирует сообщение.
Для HTTP-обработчиков net/http/httptest даёт поддельный ResponseWriter, так что обработчик можно вызвать напрямую, без сети:
func TestHealth(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/health", nil)
rec := httptest.NewRecorder()
healthHandler(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
}
if body := rec.Body.String(); body != "ok\n" {
t.Errorf("body = %q, want %q", body, "ok\n")
}
}
httptest.NewServer запускает настоящий сервер на localhost для тестирования клиентского кода. Страница про HTTP-клиент использует его в каждом примере.
Покрытие
$ go test -cover
PASS
coverage: 100.0% of statements
ok example.com/textutil 0.578s
$ go test -coverprofile=cover.out
$ go tool cover -func=cover.out
example.com/textutil/slug.go:11: Slugify 100.0%
example.com/textutil/words.go:9: CountWords 100.0%
total: (statements) 100.0%
$ go tool cover -html=cover.out # opens a browser with covered lines in green
Покрытие показывает, какие строки ни разу не выполнялись, и это помогает находить непротестированные ветки. Высокий процент не говорит о том, что проверки хорошие: тест, который вызывает каждую функцию и ничего не проверяет, достигает 100%.
Бенчмарки
Бенчмарк это func BenchmarkXxx(b *testing.B). В Go 1.24 появился b.Loop, и теперь рекомендуется именно эта форма:
func BenchmarkSlugify(b *testing.B) {
for b.Loop() {
Slugify("The Go Programming Language, 2nd Edition")
}
}
b.Loop выполняет тело столько раз, сколько нужно для стабильного измерения, исключает из замера подготовительный код до цикла и не даёт компилятору выбросить вызов при оптимизации. Код, написанный до Go 1.24, использует for i := 0; i < b.N; i++; это по-прежнему работает, но требует b.ResetTimer() после дорогой подготовки и может быть обмануто удалением мёртвого кода.
Обычный go test бенчмарки не запускает. Попросите об этом явно, а обычные тесты пропустите через -run='^$':
$ go test -bench=. -benchmem -run='^$'
goos: darwin
goarch: arm64
pkg: example.com/textutil
cpu: Apple M4
BenchmarkSlugify-10 5061945 238.1 ns/op 168 B/op 5 allocs/op
PASS
ok example.com/textutil 1.410s
Колонки: имя с добавленным GOMAXPROCS, число выполненных итераций, время на вызов, байты, выделенные на вызов, аллокации на вызов. Числа зависят от машины; сравнивайте запуски на одной и той же. Чтобы надёжно сравнить до и после изменения, запустите каждую сторону несколько раз с -count=10 и передайте оба вывода в benchstat (golang.org/x/perf/cmd/benchstat).
Примеры это тоже тесты
Функция-пример что-то печатает и объявляет ожидаемый вывод в комментарии. go test запускает её и проваливается, если вывод отличается, а go doc и pkg.go.dev показывают её как документацию:
// example_test.go
package textutil_test
import (
"fmt"
"example.com/textutil"
)
func ExampleSlugify() {
fmt.Println(textutil.Slugify("Hello, World!"))
// Output: hello-world
}
Имя пакета textutil_test делает это внешним тестом: он может использовать только экспортированный API, как настоящий вызывающий код. Такие файлы могут лежать в той же папке, что и пакет. Без комментария // Output: пример компилируется, но не запускается. Используйте // Unordered output:, когда строки могут идти в любом порядке.
Фаззинг вкратце
В Go 1.18 появились фазз-тесты, которые генерируют входные данные, чтобы находить падения и нарушенные инварианты:
func FuzzSlugify(f *testing.F) {
f.Add("Hello, World!") // seed input
f.Fuzz(func(t *testing.T, s string) {
slug := Slugify(s)
if strings.Contains(slug, "--") || strings.HasPrefix(slug, "-") {
t.Errorf("Slugify(%q) = %q: bad hyphens", s, slug)
}
})
}
Обычный go test запускает только начальные входные данные (значения f.Add и сохранённые файлы в testdata/fuzz). go test -fuzz=FuzzSlugify продолжает генерировать новые, пока не найдёт провал или вы его не остановите, и сохраняет проваливающиеся входные данные в testdata/fuzz, чтобы они стали обычными тестовыми случаями.
Частые ошибки
- Файл теста не там или не так назван. Он должен заканчиваться на
_test.goи лежать в папке пакета.slug_tests.goкомпилируется в пакет, а не запускается как тесты. - Строчная буква после
Test.Testslugifyне тест.TestSlugifyиTest_slugifyтесты. t.Fatalиз другой горутины. Там сообщайте черезt.Errorи дожидайтесь горутины до возврата из теста.- Сообщения без входных данных. Указывайте, что передали, что получили и что ожидали.
- Доверие к прохождению из кеша. Используйте
-count=1, когда тест зависит от чего-то вне пакета. - Тесты, зависящие от порядка друг друга или общих глобальных переменных. Каждый тест должен сам готовить своё состояние, для этого и существуют
t.TempDir,t.Setenvиt.Cleanup.
Часто задаваемые вопросы
Как написать юнит-тест в Go?
Создайте файл с окончанием _test.go в той же папке, что и код, импортируйте testing и напишите функцию func TestName(t *testing.T), которая вызывает ваш код и сообщает о несовпадениях через t.Errorf. Запустите go test в этой папке или go test ./... для всего модуля. Ни фреймворк, ни библиотека утверждений не нужны.
Чем t.Error отличается от t.Fatal в Go?
t.Error и t.Errorf помечают тест как проваленный и продолжают его выполнение, так что один запуск может сообщить о нескольких проблемах. t.Fatal и t.Fatalf помечают тест проваленным и сразу его останавливают. Используйте Fatal, когда продолжать нет смысла, например после неожиданной ошибки, из-за которой результат непригоден.
Как запустить один тест в Go?
Передайте регулярное выражение в -run: go test -run TestSlugify запускает все тесты с подходящим именем, а go test -run 'TestSlugifyTable/punctuation' запускает один подтест (пробелы в именах подтестов превращаются в подчёркивания). Добавьте -v, чтобы видеть имя и результат каждого теста, и -count=1, чтобы обойти кеш тестов.
Как написать бенчмарк в Go?
Напишите func BenchmarkName(b *testing.B) в файле _test.go и поместите измеряемый код в цикл for b.Loop() { ... } (Go 1.24; старый код использует for i := 0; i < b.N; i++). Запустите через go test -bench=. -benchmem, и он сообщит наносекунды, байты и число аллокаций на операцию.
Как посмотреть покрытие тестами в Go?
go test -cover печатает процент инструкций, выполненных тестами. Для подробностей запишите профиль через go test -coverprofile=cover.out, затем запустите go tool cover -func=cover.out для цифр по функциям или go tool cover -html=cover.out, чтобы увидеть покрытые и непокрытые строки в браузере.