Go: Тестирование

23 вопросов

1 Что такое пакет testing в Go? Как пишут тесты?

Пакет testing - стандартный фреймворк для тестов. Файлы тестов заканчиваются на _test.go, функции тестов имеют сигнатуру func TestXxx(t *testing.T). Запуск: go test ./.... Тест проваливается при вызове t.Error, t.Fatal или при панике.

func TestAdd(t *testing.T) {
    if got := Add(2, 3); got != 5 {
        t.Errorf("Add(2,3) = %d; want 5", got)
    }
}
Открыть отдельно →
2 Что такое TestMain? Когда использовать?

TestMain(m *testing.M) - точка входа для набора тестов в пакете. Вызывается один раз; внутри можно выполнить setup/teardown (поднять БД, замокать конфиг), затем os.Exit(m.Run()). Используют для глобальной инициализации и очистки.

func TestMain(m *testing.M) {
    setup()
    code := m.Run()
    teardown()
    os.Exit(code)
}
Открыть отдельно →
3 Как организовать подтесты через t.Run?

t.Run(name, func(t *testing.T) { ... }) запускает подтест с именем name. Позволяет группировать проверки и видеть результат по каждому подтесту. Можно запускать выборочно: go test -run TestFoo/bar.

func TestSplit(t *testing.T) {
    t.Run("empty", func(t *testing.T) {
        got := Split("", ",")
        if len(got) != 0 { t.Fatal("want empty") }
    })
    t.Run("normal", func(t *testing.T) {
        got := Split("a,b", ",")
        if len(got) != 2 { t.Fatal("want 2") }
    })
}
Открыть отдельно →
4 Что такое test fixtures в Go?

Фикстуры - заранее подготовленные данные и окружение для тестов (файлы, каталоги, записи в БД). В Go часто создают в testdata/ или в коде (временные файлы через os.CreateTemp, хелперы типа setupDB(t)). Чистка - в t.Cleanup или defer.

func TestReadConfig(t *testing.T) {
    dir := t.TempDir()
    path := filepath.Join(dir, "config.json")
    os.WriteFile(path, []byte("{\"key\":\"val\"}"), 0644)
    cfg, err := ReadConfig(path)
    if err != nil { t.Fatal(err) }
}
Открыть отдельно →
5 Что такое table-driven tests в Go?

Один тест проверяет много кейсов: слайс структур с входом и ожидаемым результатом, цикл по кейсам и вызов t.Run(tc.name, ...). Удобно добавлять кейсы и избегать дублирования. Имена кейсов делают осмысленными для отчета.

func TestAdd(t *testing.T) {
    cases := []struct{ a, b, want int }{
        {1, 2, 3}, {0, 0, 0}, {-1, 1, 0},
    }
    for _, tc := range cases {
        t.Run(fmt.Sprintf("%d+%d", tc.a, tc.b), func(t *testing.T) {
            if got := Add(tc.a, tc.b); got != tc.want {
                t.Errorf("got %d; want %d", got, tc.want)
            }
        })
    }
}
Открыть отдельно →
6 Зачем нужен t.Helper()?

t.Helper() помечает функцию как хелпер теста: при вызове t.Error/t.Fatal номер строки в отчете будет в месте вызова хелпера, а не внутри него. Упрощает отладку при использовании общих проверок.

func assertEqual(t *testing.T, got, want int) {
    t.Helper()
    if got != want {
        t.Errorf("got %d; want %d", got, want)
    }
}
Открыть отдельно →
7 Что делает t.Parallel()?

t.Parallel() помечает тест как параллельный: он будет выполняться одновременно с другими параллельными тестами в пакете. Ускоряет прогон. Важно: тесты не должны делить изменяемое состояние; для изоляции используют свои данные или блокировки.

func TestA(t *testing.T) { t.Parallel(); ... }
func TestB(t *testing.T) { t.Parallel(); ... }
Открыть отдельно →
8 Как пишут бенчмарки в Go? Что такое testing.B?

Бенчмарк - функция func BenchmarkXxx(b *testing.B). В цикле for i := 0; i < b.N; i++ выполняется измеряемый код; b.N подбирается автоматически. Запуск: go test -bench=. -benchmem. testing.B дает методы ReportAllocs(), ResetTimer().

func BenchmarkConcat(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = Concat("a", "b")
    }
}
Открыть отдельно →
9 Что такое golden files в тестах?

Golden file - эталонный файл с ожидаемым выводом. Тест выполняет код, читает вывод из файла (например testdata/golden.txt) и сравнивает. При изменении поведения обновляют golden вручную или флагом (например -update). Удобно для сложного вывода (HTML, JSON, многострочный текст).

golden := filepath.Join("testdata", "output.golden")
if *update { os.WriteFile(golden, got, 0644); return }
want, _ := os.ReadFile(golden)
if !bytes.Equal(got, want) { t.Errorf("mismatch") }
Открыть отдельно →
10 Как использовать testcontainers в Go?

testcontainers-go поднимает реальные контейнеры (Postgres, Redis, Kafka) на время тестов. Создают контейнер через testcontainers.GenericContainer, получают хост и порт, подключаются из теста. После теста контейнер останавливается. Подходит для интеграционных тестов без моков.

ctx := context.Background()
req := testcontainers.ContainerRequest{Image: "postgres:15", ...}
pg, _ := testcontainers.GenericContainer(ctx, req)
pg.Start(ctx)
host, _ := pg.Host(ctx)
port, _ := pg.MappedPort(ctx, "5432")
// use host:port in test
defer pg.Terminate(ctx)
Открыть отдельно →
11 Как тестировать HTTP-обработчики с httptest?

Пакет net/http/httptest дает httptest.NewRequest и httptest.NewRecorder. Создают запрос, вызывают handler.ServeHTTP(rec, req), проверяют rec.Code, rec.Body. Сервер поднимать не нужно - тест изолированный и быстрый.

req := httptest.NewRequest("GET", "/health", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != 200 { t.Errorf("code %d", rec.Code) }
if !strings.Contains(rec.Body.String(), "ok") { t.Error("body") }
Открыть отдельно →
12 Как считать покрытие кода тестами в Go?

Запуск с флагом: go test -cover ./.... Покрытие в процентах по пакету. Детальный отчет: go test -coverprofile=coverage.out ./..., затем go tool cover -html=coverage.out - откроется браузер с подсветкой непокрытых строк.

go test -cover -coverprofile=out ./pkg
go tool cover -func=out   // по функциям
go tool cover -html=out    // HTML отчет
Открыть отдельно →
13 Что такое тестирование в пакете _test (external test)?

Тесты в пакете foo_test (объявление package foo_test) видят только экспортированные имена пакета foo. Так проверяют публичный API "снаружи", как сторонний код. Внутренние тесты (package foo) видят неэкспортированные символы. External тесты помогают избежать циклических зависимостей.

// foo_test.go
package foo_test
import "mymod/foo"
func TestPublicAPI(t *testing.T) {
    r := foo.NewReader()  // только экспорт
}
Открыть отдельно →
14 Как генерировать моки в Go? mockgen, gomock.

mockgen (из go generate) генерирует мок-реализации интерфейсов. Указывают исходный интерфейс и пакет; генерируется тип с методами EXPECT() для задания ожиданий. gomock - библиотека для контроля вызовов (количество, аргументы, возвраты). Удобно для тестов с зависимостями.

//go:generate mockgen -destination=mock_repo.go -package=pkg mymod Repository
type Repository interface { Get(id int) (*User, error) }
// в тесте:
ctrl := gomock.NewController(t)
mockRepo := NewMockRepository(ctrl)
mockRepo.EXPECT().Get(1).Return(user, nil)
Открыть отдельно →
15 Как тестировать код с базой данных в Go?

Варианты: 1) in-memory SQLite или тестовая БД с миграциями. 2) testcontainers для реальной Postgres/MySQL. 3) интерфейс репозитория и мок в unit-тестах. 4) транзакция с откатом в конце теста (изоляция). Часто делают хелпер setupTestDB(t), возвращающий пул и путь к DSN.

db := setupTestDB(t)
tx, _ := db.BeginTx(ctx, nil)
defer tx.Rollback()
repo := NewRepo(tx)
// тесты против repo
}
Открыть отдельно →
16 Что такое t.Cleanup? Чем лучше defer?

t.Cleanup(fn) регистрирует функцию, которая выполнится после завершения теста (включая подтесты). Порядок выполнения - LIFO (как defer). Удобнее defer при использовании хелперов: хелпер может зарегистрировать очистку, не возвращая ресурс вызывающему. Рекомендуется вместо отдельного tearDown в табличных тестах.

func setup(t *testing.T) *DB {
    db := openDB(t)
    t.Cleanup(func() { db.Close() })
    return db
}
Открыть отдельно →
17 Когда использовать t.Skip?

t.Skip(msg) пропускает тест с сообщением. Тест помечается как skipped, не проваливается. Используют для тестов, требующих окружения (интеграция с внешним API, специфичная ОС), или временно отключенных тестов. Можно проверять условие: if runtime.GOOS != "linux" { t.Skip("linux only") }.

if os.Getenv("INTEGRATION") == "" {
    t.Skip("set INTEGRATION=1 to run")
}
Открыть отдельно →
18 Как запускать тесты с race detector?

Флаг -race включает детектор гонок: go test -race ./.... Приложение перекомпилируется с инструментацией; при обнаружении data race тест падает с трассой. Используют в CI и локально для тестов конкурентности. Нагрузка и время выполнения увеличиваются.

go test -race -count=1 ./...
go run -race ./cmd/server
Открыть отдельно →
19 Что такое fuzzing в Go (с Go 1.18)?

Fuzzing - автоматическая подача случайных входов в функцию. Функция вида func FuzzXxx(f *testing.F), добавление seed-кейсов через f.Add, в цикле f.Fuzz(func(t *testing.T, a, b []byte) { ... }). Запуск: go test -fuzz=FuzzName -fuzztime=30s. Находит краевые случаи и паники.

func FuzzParse(f *testing.F) {
    f.Add([]byte("key=value"))
    f.Fuzz(func(t *testing.T, data []byte) {
        if _, err := Parse(data); err != nil { return }
    })
}
Открыть отдельно →
20 Что такое testing.Short? Зачем нужен?

Флаг testing.Short() возвращает true, если тесты запущены с -short. Используют чтобы в "коротком" режиме пропускать долгие или интеграционные тесты: if testing.Short() { t.Skip("skipping in short mode") }. В CI часто запускают без -short, локально - с -short для быстрого прогона.

func TestHeavy(t *testing.T) {
    if testing.Short() { t.Skip("heavy test") }
    // long setup and test
}
Открыть отдельно →
21 Плюсы и минусы testify (assert, require)?

testify дает assert и require: assert.Equal(t, expected, got), require.NoError(t, err). Плюсы: читаемые сообщения об ошибках, много готовых матчеров. Минусы: дополнительная зависимость, не идиоматичный для стандартной библиотеки стиль. В Go принято писать простые if got != want { t.Errorf(...) }.

assert.Equal(t, 42, result)
require.NotNil(t, obj)
assert.Contains(t, slice, item)
Открыть отдельно →
22 Чем интеграционные тесты отличаются от unit в Go?

Unit-тесты проверяют одну единицу (функцию, тип) в изоляции; зависимости подменяются моками или фейками. Интеграционные тесты проверяют взаимодействие компонентов: реальная БД, HTTP-сервер, очередь. В Go unit обычно в _test.go рядом с кодом; интеграционные часто в отдельном пакете или за флагом/env (testcontainers, реальный порт).

// unit
mockDB := NewMockDB()
svc := NewService(mockDB)

// integration
db := testcontainers.Postgres(ctx)
svc := NewService(db)
Открыть отдельно →
23 Какие соглашения по именованию тестов в Go?

Функции: TestXxx(t *testing.T) для тестов, BenchmarkXxx(b *testing.B) для бенчмарков, FuzzXxx(f *testing.F) для fuzz. Имена должны быть осмысленными; для table-driven часто используют t.Run(tc.name, ...) или fmt.Sprintf("%d+%d", tc.a, tc.b). Примеры (ExampleXxx) выводятся в документации. Файлы: *_test.go.

func TestParse_InvalidInput_ReturnsError(t *testing.T) { }
func BenchmarkSort(b *testing.B) { }
Открыть отдельно →
🧠Квиз 🏆Лидеры 🎯Собесед. 📖Вопросы 📚База зн.