Содержание

Тестирование Go-кода

Тесты нужны не для галочки и не только для CI. Хороший тест фиксирует ожидание: если функция получает такие данные, она должна вернуть такой результат или такую ошибку. Когда вы меняете код, тесты показывают, что важное поведение осталось прежним.

В Go тесты пишутся без отдельного фреймворка. Достаточно стандартного пакета testing и файлов с суффиксом _test.go.

Первый тест

Код:

func Add(a, b int) int { return a + b }

Тест:

func TestAdd(t *testing.T) { got := Add(2, 3) want := 5 if got != want { t.Fatalf("Add(2, 3) = %d, want %d", got, want) } }

Запуск:

go test ./...

Функция теста должна начинаться с Test, принимать *testing.T и лежать в файле _test.go.

got и want

В Go-тестах часто используют имена got и want:

got := NormalizeName(" Alice ") want := "alice" if got != want { t.Fatalf("got %q, want %q", got, want) }

Так сообщение об ошибке сразу показывает фактический и ожидаемый результат.

Table-driven tests

Если случаев несколько, используйте таблицу:

func TestIsValidPort(t *testing.T) { tests := []struct { name string port int want bool }{ {name: "zero", port: 0, want: false}, {name: "http", port: 80, want: true}, {name: "too high", port: 70000, want: false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := IsValidPort(tt.port) if got != tt.want { t.Fatalf("got %v, want %v", got, tt.want) } }) } }

Так легче добавлять новые случаи и понимать, какой сценарий сломался.

Проверка ошибок

Если ошибка ожидается:

_, err := ParseConfig("missing.yaml") if err == nil { t.Fatal("expected error, got nil") }

Если важен тип ошибки:

if !errors.Is(err, os.ErrNotExist) { t.Fatalf("expected os.ErrNotExist, got %v", err) }

Не сравнивайте текст ошибки без необходимости. Текст может измениться, а смысл ошибки остаться тем же.

t.Helper

Если вы выносите повторяющуюся проверку в функцию, добавьте t.Helper():

func requireNoError(t *testing.T, err error) { t.Helper() if err != nil { t.Fatalf("unexpected error: %v", err) } }

Тогда Go покажет строку, где helper был вызван, а не строку внутри helper.

Временные файлы

Не создавайте тестовые файлы вручную в проекте. Используйте t.TempDir():

func TestReadFile(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "data.txt") err := os.WriteFile(path, []byte("hello"), 0644) if err != nil { t.Fatal(err) } got, err := ReadFile(path) if err != nil { t.Fatal(err) } if got != "hello" { t.Fatalf("got %q", got) } }

Go сам удалит временную папку после теста.

Тестирование через интерфейсы

Если сервис зависит от внешнего API или базы, спрячьте зависимость за интерфейсом:

type WeatherClient interface { Current(ctx context.Context, city string) (Weather, error) }

В тесте можно использовать fake:

type fakeWeatherClient struct { weather Weather } func (f fakeWeatherClient) Current(ctx context.Context, city string) (Weather, error) { return f.weather, nil }

Так тест бизнес-логики не зависит от интернета.

Тестирование конкурентного кода

Для поиска гонок:

go test -race ./...

Флаг -race замедляет запуск, но хорошо ловит ошибки общего состояния. Используйте его для кода с goroutine, mutex, channels и shared maps.

Покрытие

go test -cover ./...

Покрытие полезно как индикатор, но не как цель. Лучше 60% осмысленных тестов, чем 95% проверок, которые ничего не ловят.

Для HTML-отчета:

go test -coverprofile=coverage.out ./... go tool cover -html=coverage.out

Benchmarks

Benchmark измеряет производительность:

func BenchmarkNormalizeName(b *testing.B) { for i := 0; i < b.N; i++ { NormalizeName(" Alice ") } }

Запуск:

go test -bench=. ./...

Не начинайте оптимизацию с benchmark, если нет проблемы. Но когда проблема есть, benchmark помогает сравнивать решения.

Частые команды

go test ./... go test ./internal/parser go test -run TestParse ./... go test -race ./... go test -cover ./... go test -v ./... go test -bench=. ./...

Что тестировать первым

  1. Парсинг входных данных.
  2. Валидацию.
  3. Алгоритмы.
  4. Работу с ошибками.
  5. Бизнес-правила.
  6. Код, который легко сломать при рефакторинге.

UI, сеть и внешние API лучше отделять интерфейсами, чтобы тестировать бизнес-логику без реального интернета.

Следующий шаг после статьи

Закрепите тему во вводном проекте без регистрации, а затем переходите к курсам.

Продолжить изучение

Выбери следующую статью по маршруту или углубись в смежную тему.

Похожие статьи