Перейти к содержимому

Продвинутое тестирование в Go

Зачем знать: На уровне middle 1 одного func TestFoo(t *testing.T) уже недостаточно. Команды требуют параллельные тесты, контейнеры с реальной БД, моки внешних сервисов, golden-файлы, генераторы. Без этих навыков не получится поддерживать тесты в большом проекте: они будут медленными, flaky и непредсказуемыми. На собесе спрашивают про t.Parallel, t.Helper, gomock/testify, testcontainers, coverage режимы — это рабочие инструменты, а не теория.

  1. Базовая концепция
  2. Под капотом / Best practices
  3. Gotchas
  4. Производительность
  5. Вопросы на собеседовании
  6. Practice
  7. Источники

Тестирование в Go опирается на testing пакет stdlib. Middle 1 уровень означает:

  • t.Parallel() — параллельные тесты внутри одного бинаря (go test).
  • t.Cleanup() — регистрация cleanup-функций после теста.
  • t.Helper() — пометка вспомогательной функции для корректных номеров строк в failure.
  • Table-driven tests — каноничный паттерн в Go.
  • testify, gomock, mockery — экосистема моков.
  • httptest — тесты HTTP-сервисов без реальной сети.
  • testcontainers-go — реальные контейнеры (Postgres, Redis) для интеграционных тестов.
  • Golden-файлы, snapshot тесты — фиксация ожидаемого вывода.

Цель: писать быстрые, детерминированные, изолированные тесты, которые не падают раз в N запусков.

Минимальный пример с расширенными возможностями

Заголовок раздела «Минимальный пример с расширенными возможностями»
package mypkg_test
import (
"context"
"testing"
"time"
)
func TestProcess_TableParallel(t *testing.T) {
t.Parallel() // верхний тест параллелится с другими TestXxx
cases := []struct {
name string
input int
want int
wantErr bool
}{
{"zero", 0, 0, false},
{"positive", 5, 25, false},
{"negative", -3, 0, true},
}
for _, tc := range cases {
tc := tc // Go 1.22+ не требует, но в legacy-кодах надо
t.Run(tc.name, func(t *testing.T) {
t.Parallel() // sub-test тоже параллельный
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
t.Cleanup(cancel) // вызовется ПОСЛЕ всех defer внутри теста
got, err := Process(ctx, tc.input)
if (err != nil) != tc.wantErr {
t.Fatalf("err=%v wantErr=%v", err, tc.wantErr)
}
if got != tc.want {
t.Errorf("got=%d want=%d", got, tc.want)
}
})
}
}

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

  1. Тест вызывает t.Parallel(), приостанавливает себя через канал.
  2. Когда все непараллельные тесты в пакете отработают, runner запускает приостановленные параллельно (до -parallel=GOMAXPROCS штук одновременно).
  3. Все subtests с t.Parallel() внутри одного родителя ждут завершения всех siblings перед родительским cleanup.

Правила:

  • Кол-во параллельности регулируется флагом -parallel N (по умолчанию runtime.GOMAXPROCS(0)).
  • Пакеты запускаются параллельно через -p N (по умолчанию runtime.GOMAXPROCS(0)). Это другой флаг — параллельность между пакетами.
  • t.Parallel внутри table-driven — стандартный паттерн, но требует осторожности с замыканием:
for _, tc := range cases {
// Go 1.21 и раньше: tc := tc обязательно (loop variable capture)
// Go 1.22+: loop variable новая на каждой итерации, можно не делать
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// tc безопасно
})
}

В Go 1.22 исправили ловушку с for циклом (FRT-3022, FRT-3024 спецификации). Раньше переменная итерации была общей; теперь — новая на каждой итерации. Но! Линтеры (copyloopvar, loopclosure) всё ещё могут жаловаться в проектах с go.mod ниже 1.22 — следите за директивой go 1.22 в go.mod, она и включает новый scope.

func TestFoo(t *testing.T) {
db := openDB()
defer db.Close() // ПЕРВОЕ выполнится при выходе из TestFoo
t.Cleanup(func() {
log.Println("cleanup 1") // ВТОРОЕ
})
t.Cleanup(func() {
log.Println("cleanup 2") // ТРЕТЬЕ — cleanup LIFO
})
// тело теста
}

Ключевые отличия:

Свойствоdefert.Cleanup
ПорядокLIFOLIFO
Выполняется когдаПри выходе из функцииПосле теста (включая parallel siblings)
Помощь testify suiteНетДа: t.Cleanup интегрируется с t.Parallel
Видимость panicrecover() нужен в deferCleanup сам не подавляет panic
Можно вызвать из t.HelperНет (defer привязан к функции)Да (cleanup живёт на уровне *testing.T)

Главное правило: для ресурсов, которые надо освободить после того, как parallel-сабтесты доработают, использовать t.Cleanup, а не defer. Иначе ресурс закроется до того, как параллельные дети используют его.

func TestDB(t *testing.T) {
db := openDB()
// НЕПРАВИЛЬНО: db закроется до того, как parallel sub-tests доработают
// defer db.Close()
t.Cleanup(func() { db.Close() }) // правильно
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// использует db
})
}
}

Без t.Helper() номер строки в t.Fatal/Error указывает на саму вспомогательную функцию, а не на место вызова. С t.Helper() Go runtime пропускает кадр стека при формировании сообщения об ошибке.

func assertEq[T comparable](t *testing.T, got, want T) {
t.Helper() // обязательно
if got != want {
t.Errorf("got=%v want=%v", got, want)
}
}
func TestSomething(t *testing.T) {
assertEq(t, 1, 2) // ошибка покажет ЭТУ строку, а не assertEq
}

Под капотом: t.Helper() добавляет имя текущей функции в helperPCs поле common, и при decorate в testing пропускает эти кадры.

Самая популярная библиотека для assertions в РФ. Установка: go get github.com/stretchr/testify.

import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAssertVsRequire(t *testing.T) {
// assert — продолжает тест после неудачи (как t.Errorf)
assert.Equal(t, 5, calc())
assert.NotNil(t, obj)
// тест продолжится, даже если падёт
// require — останавливает тест (как t.Fatalf)
require.NoError(t, err) // если err != nil, тест дальше не пойдёт
require.NotNil(t, result)
result.UseSafely() // безопасно, потому что require прошёл
}

Правило: для проверки пререкизитов (nil, err) — require; для бизнес-чеков, где хочется увидеть все провалы сразу — assert.

import "github.com/stretchr/testify/suite"
type RepoSuite struct {
suite.Suite
db *sql.DB
}
func (s *RepoSuite) SetupSuite() { s.db = openDB() }
func (s *RepoSuite) TearDownSuite() { s.db.Close() }
func (s *RepoSuite) SetupTest() { s.db.Exec("TRUNCATE users") }
func (s *RepoSuite) TestInsert() {
err := Insert(s.db, "alice")
s.Require().NoError(err)
s.Equal(1, count(s.db))
}
func TestRepo(t *testing.T) { suite.Run(t, new(RepoSuite)) }

suite удобен, когда у группы тестов общая инициализация, но по умолчанию не запускает методы параллельно. Для параллельности — каждый сам себя помечает, плюс осторожно с shared state.

import "github.com/stretchr/testify/mock"
type MockNotifier struct{ mock.Mock }
func (m *MockNotifier) Send(ctx context.Context, msg string) error {
args := m.Called(ctx, msg)
return args.Error(0)
}
func TestUseCase(t *testing.T) {
m := new(MockNotifier)
m.On("Send", mock.Anything, "hello").Return(nil).Once()
err := UseCase(m, "hello")
require.NoError(t, err)
m.AssertExpectations(t)
}

Простой вариант, но нет проверки сигнатуры на компиляции — опечатался в имени метода, тест собирается, но в рантайме mock.AssertCalled упадёт. Поэтому для критичных интерфейсов используют gomock/mockery.

В 2023 году Google заархивировал golang/mock. Сообщество форкнуло как go.uber.org/mock, в 2026 это де-факто стандарт для генерируемых моков.

Установка mockgen:

Окно терминала
go install go.uber.org/mock/mockgen@latest

Генерация:

//go:generate mockgen -source=notifier.go -destination=mocks/notifier_mock.go -package=mocks
type Notifier interface {
Send(ctx context.Context, to, msg string) error
}

Использование:

import "go.uber.org/mock/gomock"
func TestUseCase(t *testing.T) {
ctrl := gomock.NewController(t) // t.Cleanup внутри регистрирует Finish
m := mocks.NewMockNotifier(ctrl)
m.EXPECT().
Send(gomock.Any(), "alice@example.com", gomock.Eq("hi")).
Return(nil).
Times(1)
err := DoStuff(m)
require.NoError(t, err)
}

В новых версиях gomock.NewController(t) сам регистрирует t.Cleanup(ctrl.Finish) — больше не нужен явный defer ctrl.Finish() (что было обязательно в golang/mock).

Matcher’ы:

gomock.Any() // любой аргумент
gomock.Eq(x) // reflect.DeepEqual
gomock.Nil() // nil
gomock.Not(gomock.Eq(0)) // отрицание
gomock.AssignableToTypeOf(0) // по типу
gomock.InAnyOrder(...) // порядок не важен
// custom: реализовать interface Matcher

Ожидание порядка через .After():

first := m.EXPECT().Connect().Return(nil)
m.EXPECT().Disconnect().After(first)

Альтернатива gomock — mockery (vektra/mockery), генерирует mock на базе testify/mock. Минус: тесты выглядят как mock.Anything, плюс: совместим с testify ассертами.

Окно терминала
go install github.com/vektra/mockery/v2@latest
mockery --name=Notifier --output=mocks
m := mocks.NewNotifier(t) // в v2.30+ принимает testing.TB и сам делает Cleanup
m.On("Send", mock.Anything, "alice", "hi").Return(nil)

Выбор: gomock — компилируемая типобезопасность, mockery — простота и интеграция с testify. На больших проектах обычно gomock (or uber-go/mock).

net/http/httptest — два основных режима:

  1. httptest.NewServer(handler) — поднимает реальный listener на случайном порту.
  2. httptest.NewRecorder()http.ResponseWriter в памяти.
func TestClient(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/items/42", r.URL.Path)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"id":42}`))
}))
t.Cleanup(srv.Close)
c := NewClient(srv.URL)
item, err := c.GetItem(context.Background(), 42)
require.NoError(t, err)
assert.Equal(t, 42, item.ID)
}

srv.URLhttp://127.0.0.1:RANDOM_PORT. Для TLS — NewTLSServer, доступен srv.Certificate().

func TestHandler_GetUser(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/users/42", nil)
req = req.WithContext(context.Background())
w := httptest.NewRecorder()
handler := NewHandler(repo)
handler.ServeHTTP(w, req)
res := w.Result()
defer res.Body.Close()
assert.Equal(t, http.StatusOK, res.StatusCode)
var u User
require.NoError(t, json.NewDecoder(res.Body).Decode(&u))
assert.Equal(t, 42, u.ID)
}
func TestAuthMiddleware(t *testing.T) {
h := AuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value(userKey).(string)
w.Write([]byte(user))
}))
t.Run("no token", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
assert.Equal(t, 401, w.Code)
})
t.Run("valid token", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Authorization", "Bearer good")
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, "alice", w.Body.String())
})
}

Для интеграционных тестов с реальной БД/Redis/Kafka. Работает поверх Docker.

import (
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/postgres"
"github.com/testcontainers/testcontainers-go/wait"
)
func setupPostgres(t *testing.T) string {
ctx := context.Background()
container, err := postgres.Run(ctx,
"postgres:16-alpine",
postgres.WithDatabase("test"),
postgres.WithUsername("postgres"),
postgres.WithPassword("secret"),
testcontainers.WithWaitStrategy(
wait.ForLog("database system is ready to accept connections").
WithOccurrence(2). // PG логирует это дважды
WithStartupTimeout(30*time.Second),
),
)
require.NoError(t, err)
t.Cleanup(func() {
if err := container.Terminate(ctx); err != nil {
t.Logf("terminate: %v", err)
}
})
dsn, err := container.ConnectionString(ctx, "sslmode=disable")
require.NoError(t, err)
return dsn
}
func TestRepo_Integration(t *testing.T) {
if testing.Short() {
t.Skip("integration test")
}
dsn := setupPostgres(t)
db, err := sql.Open("pgx", dsn)
require.NoError(t, err)
t.Cleanup(func() { db.Close() })
require.NoError(t, runMigrations(db))
err = InsertUser(db, "alice")
require.NoError(t, err)
}

Контейнер можно переиспользовать между запусками тестов (ускоряет dev-loop):

container, err := postgres.Run(ctx, "postgres:16",
postgres.WithDatabase("test"),
testcontainers.WithReuse(), // не убивать
testcontainers.WithName("test-pg"), // имя для reuse
)
network, err := testcontainers.GenericNetwork(ctx, testcontainers.GenericNetworkRequest{
NetworkRequest: testcontainers.NetworkRequest{Name: "test-net"},
})
require.NoError(t, err)
t.Cleanup(func() { network.Remove(ctx) })
// у каждого контейнера: Networks: []string{"test-net"}, NetworkAliases: map[string][]string{"test-net": {"pg"}}
// внутри сети контейнеры видят друг друга по alias "pg"

Принципы:

  1. Нет сетевых вызовов к внешним сервисам (только httptest или контейнеры).
  2. Нет shared state — каждый тест строит/чистит данные сам.
  3. Нет реального времени — используем clock.Mock или передаваемое time.Now.
  4. Нет окружения — не читаем os.Getenv без mock’а.
  5. Детерминизмmath/rand с фиксированным seed, отсортированные ключи map’ов.
// Плохо: непредсказуемо
func TestRand(t *testing.T) {
if rand.Intn(10) < 5 {
t.Fatal("flaky") // иногда падает
}
}
// Хорошо: явный rand source
func TestRand(t *testing.T) {
r := rand.New(rand.NewSource(42))
assert.Equal(t, 6, r.Intn(10))
}

Паттерн для проверки сложного вывода (JSON, HTML, генерируемый код):

testdata/
golden/
user_response.json
var update = flag.Bool("update", false, "update golden files")
func TestRender(t *testing.T) {
got := RenderUser(User{ID: 1, Name: "alice"})
goldenPath := filepath.Join("testdata", "golden", "user_response.json")
if *update {
require.NoError(t, os.WriteFile(goldenPath, got, 0644))
}
want, err := os.ReadFile(goldenPath)
require.NoError(t, err)
assert.Equal(t, string(want), string(got))
}
// Обновление: go test -update

cupaloy автоматизирует golden-файлы:

import "github.com/bradleyjkemp/cupaloy/v2"
func TestSnapshot(t *testing.T) {
out := generate()
cupaloy.SnapshotT(t, out) // создаст .snapshots/ при первом запуске
}
// UPDATE_SNAPSHOTS=true go test
Окно терминала
go test -cover -covermode=set # был ли блок выполнен (по умолчанию)
go test -cover -covermode=count # сколько раз
go test -cover -covermode=atomic # как count, но safe для -race

Многопакетный coverage:

Окно терминала
go test -cover -coverpkg=./... ./pkg/...
go test -coverprofile=cover.out ./...
go tool cover -html=cover.out
go tool cover -func=cover.out

С Go 1.20 добавлена возможность собирать покрытие не только в go test, а из любого скомпилированного бинаря:

Окно терминала
go build -cover -o myserver ./cmd/server
GOCOVERDIR=cov ./myserver &
# ... интеграционные тесты, ходящие в myserver ...
kill %1
go tool covdata percent -i=cov
go tool covdata textfmt -i=cov -o cov.out
go tool cover -html=cov.out

Это позволяет получать реальное покрытие, собранное в E2E.

  • ginkgo — BDD-фреймворк (Describe, It, BeforeEach). Встречается в k8s-проектах, но в РФ редко.
  • gopter, leanovate/gopter — property-based testing.
  • rapid (flyingmutant/rapid) — современная альтернатива gopter, активно развивается.
import "pgregory.net/rapid"
func TestPropReverseReverse(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
s := rapid.String().Draw(t, "s")
if string(reverse(reverse([]byte(s)))) != s {
t.Fatal("not idempotent")
}
})
}
  • Benchmarks — отдельная тема (go test -bench=. -benchmem), часто запускают в CI с benchstat для регрессий.
┌──────────────────────────┐
│ E2E (10%) │ testcontainers + HTTP
├──────────────────────────┤
│ Integration (20-30%) │ testcontainers, реальная БД
├──────────────────────────┤
│ Unit (60-70%) │ моки, чистые функции
└──────────────────────────┘

В Go пакет — естественная единица тестирования. Стремитесь к: пакет проверяется в одиночку без сети/диска, интеграция — отдельные тесты с //go:build integration тегом.

Причины:

  • Зависимость от времени (time.Sleep, не передаваемое clock).
  • Гонки данных (запустите go test -race ./...).
  • Параллельные тесты делят shared state (общая БД-таблица).
  • Сетевые тесты.
  • map-итерация (порядок недетерминированный).
  • goroutine leak — предыдущий тест оставил активную goroutine, она шумит в следующих.

Лечение:

  • go test -count=100 ./pkg локально для поиска редких падений.
  • t.Parallel + изоляция данных (t.TempDir, отдельный schema/db).
  • Inject зависимости (clock, randomness, env).
  • goleak.VerifyTestMain для контроля утечек goroutine.

TestMain(m *testing.M) — точка входа в пакетные тесты, до и после всех TestXxx.

var globalDB *sql.DB
func TestMain(m *testing.M) {
flag.Parse()
if testing.Short() {
os.Exit(m.Run())
}
globalDB = setupDB()
defer globalDB.Close()
code := m.Run()
os.Exit(code)
}

⚠️ os.Exit не вызывает defer. Чтобы сработали — выносите cleanup в анонимную функцию:

func TestMain(m *testing.M) {
os.Exit(func() int {
db := setup()
defer db.Close() // сработает до os.Exit
return m.Run()
}())
}

var cache = map[string]int{}
func TestParallel(t *testing.T) {
for i := 0; i < 100; i++ {
i := i
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
t.Parallel()
cache[fmt.Sprintf("k%d", i)] = i // RACE
})
}
}

go test -race поймает.

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
process(tc.input) // в Go <1.22 ВСЕ горутины увидят последний tc!
})
}

В Go 1.22+ переменная новая на каждой итерации (при go 1.22 в go.mod). В legacy tc := tc обязательно.

Cleanup’ы parent теста ждут завершения всех его parallel subtests. Это значит:

func TestX(t *testing.T) {
res := setup()
t.Cleanup(res.Close)
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
res.Do(...) // безопасно, res доживёт
})
}
}
func TestX(t *testing.T) {
res := setup()
defer res.Close() // ОПАСНО: закроется до выполнения parallel sub-tests
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
res.Do(...) // PANIC: use after close
})
}
}

t.Skipped(), t.Failed() родителя зависят от детей; если дочерний тест провалился, родитель тоже считается провалившимся. Это иногда удивляет в логике cleanup.

httptest.Server использует http.Server с keep-alive. Если клиент держит соединение, srv.Close() может ждать. Используйте srv.CloseClientConnections() или явный Connection: close.

ok := assert.NoError(t, err)
if !ok {
return // продолжать после фатального условия некорректно
}

assert.* возвращает bool, чтобы можно было ветвиться, но обычно лучше require.*.

По умолчанию порядок вызовов не важен. Чтобы зафиксировать — gomock.InOrder(...) или .After():

gomock.InOrder(
m.EXPECT().Connect(),
m.EXPECT().SendData(),
m.EXPECT().Disconnect(),
)

В CI без Docker (например, Buildkite на host без daemon) testcontainers падает. Тег:

//go:build integration

И в Makefile:

test:
go test ./... -short
test-integration:
go test -tags=integration ./...

set/count режимы не учитывают код, который не достигается (по причине build tag, например). Используйте -coverpkg, чтобы охватить весь модуль.

В CI пускайте -race отдельным job’ом, не блокируйте PR-проверки 30-минутным race-build’ом, если тестов очень много.

t.Fail из горутины не работает: testing.T не thread-safe в плане Fatal (помечает goroutine как тестовую). t.Errorf потокобезопасен. Для Fatal из горутины:

done := make(chan struct{})
go func() {
defer close(done)
if err := work(); err != nil {
t.Errorf("err: %v", err) // НЕ Fatal
}
}()
<-done

После теста горутина могла не завершиться. Используйте go.uber.org/goleak:

func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}

  • -p — параллелизм между пакетами (по умолчанию GOMAXPROCS).
  • -parallel — параллелизм внутри пакета (для t.Parallel).

Для CI на 8 CPU при 50 пакетах:

Окно терминала
go test -p=4 -parallel=4 ./...

Снижает CPU starvation, особенно когда каждый пакет — отдельный процесс.

go test -i ./... (-i устарел) или go build ./... отдельной фазой. В современных версиях кэш билдов делает это автоматически.

Go кэширует успешные результаты по содержимому входов. Чтобы принудительно перепрогнать:

Окно терминала
go clean -testcache
GOCACHE=$(mktemp -d) go test ./...
Окно терминала
go test -bench=. -benchmem ./pkg

Если тесты медленные:

  • Профилирование: go test -cpuprofile=cpu.out, go tool pprof cpu.out.
  • Перепиши setup в TestMain или sync.Once.
  • Используйте t.Parallel.

С WithReuse() контейнер не удаляется после тестов — повторный запуск стартует за секунды вместо 5-10. В CI обычно без reuse (изолированные runners).

  • Unit (in-memory) — sqlite в RAM, дешёво, но не покрывает PG-специфику.
  • Integration (testcontainers) — реальный PG, дорого, но даёт уверенность.

Гибрид: 80% unit с in-memory + 20% integration через testcontainers.

Параллельные тесты на одной БД ломаются. Решения:

  • Отдельная схема на каждый тест (pg_temp или CREATE SCHEMA test_<rand>).
  • Отдельная БД через CREATE DATABASE.
  • Контейнер на тест (медленно).
  • Транзакция с ROLLBACK в конце.
func newSchema(t *testing.T, db *sql.DB) string {
schema := "t_" + uuid.New().String()[:8]
_, err := db.Exec("CREATE SCHEMA " + schema)
require.NoError(t, err)
t.Cleanup(func() {
db.Exec("DROP SCHEMA " + schema + " CASCADE")
})
return schema
}

Регулярные бенчмарки + benchstat для сравнения main vs PR. Утилита: golang.org/x/perf/cmd/benchstat.

Окно терминала
go test -bench=. -count=10 -run=^$ > pr.txt
git checkout main
go test -bench=. -count=10 -run=^$ > base.txt
benchstat base.txt pr.txt

1. В чём разница между t.Cleanup и defer? defer выполняется при выходе из функции; t.Cleanup — после всего теста, включая parallel subtests. Для общих ресурсов с parallel — Cleanup.

2. Зачем t.Helper()? Чтобы t.Errorf/t.Fatalf указывал на caller, а не на саму утилитарную функцию. Без неё номера строк в ошибке бесполезны.

3. Что делает t.Parallel() в subtest? Помечает sub-test как параллельный с другими помеченными. Subtest приостанавливается, ждёт окончания других sequential тестов в пакете, затем запускается параллельно с другими parallel’ами (до -parallel).

4. Что такое loop variable trap и как Go 1.22 это исправил? В Go <=1.21 переменная цикла одна на весь цикл; замыкания в goroutine/subtest видели последнее значение. В Go 1.22+ переменная новая на каждой итерации (при go 1.22 в go.mod).

5. Чем assert отличается от require в testify? assert записывает ошибку и продолжает (t.Errorf), require — прекращает тест (t.Fatalf). Require для пререкизитов, assert для проверки результата.

6. Что лучше: testify/mock или gomock? Зависит. gomock — типобезопасный (генерирует код), ловит ошибки на компиляции. testify/mock — проще, но опечатки в именах методов всплывают в рантайме.

7. Как mockgen генерирует моки? mockgen -source=foo.go парсит исходник, находит интерфейсы, генерирует структуру с gomock.Controller и EXPECT() методами. Альтернатива: mockgen -package=foo Foo (рефлексия).

8. Что делает gomock.NewController(t) в новых версиях? Создаёт контроллер и регистрирует t.Cleanup(ctrl.Finish) — раньше требовался явный defer ctrl.Finish().

9. Что такое httptest.NewRecorder? In-memory реализация http.ResponseWriter. Позволяет вызвать handler напрямую, без поднятия сервера.

10. Чем httptest.NewServer отличается от NewRecorder? NewServer поднимает реальный listener (для тестирования клиентов или integration). NewRecorder — синхронный вызов handler.ServeHTTP без сети.

11. Что даёт testcontainers-go? Запускает Docker-контейнеры из теста (Postgres, Redis, etc.). Реальная БД, изолированная среда, чистка после теста.

12. Что такое hermetic test? Тест без зависимостей от внешнего окружения: нет сети, нет shared state, нет глобального времени. Цель — детерминизм.

13. Что такое golden-файлы? Файлы с ожидаемым выводом теста. Сравниваются с реальным выводом. Обновляются флагом (-update).

14. Какие есть coverage modes?

  • set — был ли блок выполнен (default).
  • count — сколько раз.
  • atomic — count, безопасный для -race.

15. Как покрытие собирать в integration-тестах (Go 1.20+)? go build -cover -o bin, запускаем с GOCOVERDIR=cov, потом go tool covdata.

16. Что такое flaky test и как его лечить? Тест, иногда падающий без изменения кода. Лечение: убрать time.Sleep, исключить shared state, контролировать randomness, проверить goroutine leak.

17. Что делать, если параллельные тесты делят БД? Изолировать: отдельная schema/database/transaction на каждый тест, либо отдельный контейнер.

18. Зачем TestMain? Глобальный setup/teardown для пакета: m.Run(), инициализация БД, миграции, флаги.

19. Что такое property-based testing? Не пишем конкретные кейсы, а описываем свойства (например, reverse(reverse(x)) == x). Фреймворк генерирует случайные входы. В Go: rapid, gopter.

20. Что делает -race и зачем? Включает Go race detector — runtime-инструментацию для поиска data race’ов. Замедляет в 2-10x, обязателен для concurrent кода.

21. Зачем goleak? Проверка, что после тестов нет утечек goroutine. goleak.VerifyTestMain(m).

22. Что выводит go test -count=10? Запускает каждый тест 10 раз. Помогает ловить flakiness.

23. Что такое subtests и зачем? t.Run("name", func(t *testing.T) {...}) — вложенные тесты. Полезно для table-driven, parallel, фильтрации (go test -run=TestX/sub).

24. Чем t.Errorf отличается от t.Fatalf? Errorf помечает тест как провалившийся, но продолжает. FatalfErrorf + runtime.Goexit() (останавливает goroutine теста).

25. Что такое snapshot testing (cupaloy)? Автоматизированные golden-файлы: первый запуск сохраняет вывод, последующие сравнивают. Удобно для большого/сложного output.

26. Когда не использовать ginkgo? Если команда привыкла к idiomatic Go testing — ginkgo BDD-стиль чужероден; усложняет debug и часто даёт хуже сигнатуры ошибок.

27. Как тестировать handler с middleware? Соберите chain: mw1(mw2(handler)), вызывайте через httptest.NewRecorder, проверяйте status/header/body. Изолируйте каждое middleware отдельным тестом.

28. Что значит t.Skip и зачем? Пропускает тест (например, при -short или отсутствии docker). Тест не считается failed.

29. Как сделать timeout для теста? go test -timeout=30s или context.WithTimeout внутри теста. В t.Cleanup отмените контекст.

30. Что такое coverpkg? Флаг -coverpkg=./... — собирать покрытие не только текущего пакета, но и зависимостей. Полезно для интеграционных тестов.


Напишите функцию SumDigits(n int) int и тесты на 5+ кейсов с t.Run и t.Parallel. Используйте t.Cleanup для логирования времени теста.

Дан интерфейс UserRepo { Get(ctx, id) (*User, error) }. Напишите UserService.Greet(id), генерируйте мок через mockgen, проверьте успешный и error-case.

Напишите HTTP-клиент Weather.Get(city) (GET /weather?city=). Тестом проверьте через httptest.NewServer, что клиент корректно парсит JSON и обрабатывает 5xx.

Поднимите Postgres через testcontainers, накатайте миграцию (создаёт users), вставьте 3 строки, прочитайте, проверьте.

Напишите RenderReport(items []Item) []byte (JSON или CSV). Сохраните ожидаемый вывод в testdata/golden/report.json и сравните в тесте.

Создайте 2 пакета: pkg/a использует pkg/b. Тест в pkg/a. Запустите с -coverpkg=./... и подтвердите, что покрытие pkg/b ненулевое.

Реализуйте OrderSuite с SetupSuite/SetupTest/TearDownSuite для тестирования OrderRepo. Минимум 3 теста.

Добавьте TestMain с goleak.VerifyTestMain(m). Намеренно запустите goroutine без завершения — проверьте, что тест падает.


  1. Документация testing: https://pkg.go.dev/testing (актуально для Go 1.22+).
  2. Go Blog “Testing in Go: clean tests using t.Cleanup”: https://go.dev/blog/subtests.
  3. testify: https://github.com/stretchr/testify — issues и README.
  4. uber-go/mock: https://github.com/uber-go/mock — README, миграция с golang/mock.
  5. mockery v2: https://vektra.github.io/mockery/.
  6. testcontainers-go: https://golang.testcontainers.org/.
  7. Go 1.20 release notes (coverage): https://tip.golang.org/doc/go1.20#cover.
  8. Go 1.22 loop var change: https://go.dev/blog/loopvar-preview.
  9. rapid (property-based): https://pkg.go.dev/pgregory.net/rapid.
  10. goleak: https://github.com/uber-go/goleak.
  11. Книга “Learning Go” Jon Bodner (2nd ed., 2024) — глава о тестировании.