Продвинутое тестирование в Go
Зачем знать: На уровне middle 1 одного
func TestFoo(t *testing.T)уже недостаточно. Команды требуют параллельные тесты, контейнеры с реальной БД, моки внешних сервисов, golden-файлы, генераторы. Без этих навыков не получится поддерживать тесты в большом проекте: они будут медленными, flaky и непредсказуемыми. На собесе спрашивают проt.Parallel,t.Helper,gomock/testify,testcontainers, coverage режимы — это рабочие инструменты, а не теория.
Содержание
Заголовок раздела «Содержание»- Базовая концепция
- Под капотом / Best practices
- Gotchas
- Производительность
- Вопросы на собеседовании
- Practice
- Источники
1. Базовая концепция
Заголовок раздела «1. Базовая концепция»Тестирование в 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) } }) }}2. Под капотом / Best practices
Заголовок раздела «2. Под капотом / Best practices»2.1 t.Parallel(): правила и сочетание с t.Run
Заголовок раздела «2.1 t.Parallel(): правила и сочетание с t.Run»t.Parallel() сообщает планировщику тестов, что текущий тест может выполняться параллельно с другими тестами, помеченными Parallel(). Внутренний механизм:
- Тест вызывает
t.Parallel(), приостанавливает себя через канал. - Когда все непараллельные тесты в пакете отработают, runner запускает приостановленные параллельно (до
-parallel=GOMAXPROCSштук одновременно). - Все 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.
2.2 t.Cleanup vs defer
Заголовок раздела «2.2 t.Cleanup vs defer»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 }) // тело теста}Ключевые отличия:
| Свойство | defer | t.Cleanup |
|---|---|---|
| Порядок | LIFO | LIFO |
| Выполняется когда | При выходе из функции | После теста (включая parallel siblings) |
| Помощь testify suite | Нет | Да: t.Cleanup интегрируется с t.Parallel |
| Видимость panic | recover() нужен в defer | Cleanup сам не подавляет 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 }) }}2.3 t.Helper() — почему критично
Заголовок раздела «2.3 t.Helper() — почему критично»Без 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 пропускает эти кадры.
2.4 testify (assert/require/suite/mock)
Заголовок раздела «2.4 testify (assert/require/suite/mock)»Самая популярная библиотека для assertions в РФ. Установка: go get github.com/stretchr/testify.
assert vs require
Заголовок раздела «assert vs require»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.
testify/mock
Заголовок раздела «testify/mock»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.
2.5 gomock (uber-go/mock, ранее golang/mock)
Заголовок раздела «2.5 gomock (uber-go/mock, ранее golang/mock)»В 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.DeepEqualgomock.Nil() // nilgomock.Not(gomock.Eq(0)) // отрицаниеgomock.AssignableToTypeOf(0) // по типуgomock.InAnyOrder(...) // порядок не важен// custom: реализовать interface MatcherОжидание порядка через .After():
first := m.EXPECT().Connect().Return(nil)m.EXPECT().Disconnect().After(first)2.6 mockery
Заголовок раздела «2.6 mockery»Альтернатива gomock — mockery (vektra/mockery), генерирует mock на базе testify/mock. Минус: тесты выглядят как mock.Anything, плюс: совместим с testify ассертами.
go install github.com/vektra/mockery/v2@latestmockery --name=Notifier --output=mocksm := mocks.NewNotifier(t) // в v2.30+ принимает testing.TB и сам делает Cleanupm.On("Send", mock.Anything, "alice", "hi").Return(nil)Выбор: gomock — компилируемая типобезопасность, mockery — простота и интеграция с testify. На больших проектах обычно gomock (or uber-go/mock).
2.7 httptest deep
Заголовок раздела «2.7 httptest deep»net/http/httptest — два основных режима:
- httptest.NewServer(handler) — поднимает реальный listener на случайном порту.
- httptest.NewRecorder() —
http.ResponseWriterв памяти.
NewServer пример
Заголовок раздела «NewServer пример»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.URL — http://127.0.0.1:RANDOM_PORT. Для TLS — NewTLSServer, доступен srv.Certificate().
NewRecorder для handler тестов
Заголовок раздела «NewRecorder для handler тестов»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)}Тестирование middleware chains
Заголовок раздела «Тестирование middleware chains»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()) })}2.8 testcontainers-go
Заголовок раздела «2.8 testcontainers-go»Для интеграционных тестов с реальной БД/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)}Reusable containers
Заголовок раздела «Reusable containers»Контейнер можно переиспользовать между запусками тестов (ускоряет dev-loop):
container, err := postgres.Run(ctx, "postgres:16", postgres.WithDatabase("test"), testcontainers.WithReuse(), // не убивать testcontainers.WithName("test-pg"), // имя для reuse)Network между containers
Заголовок раздела «Network между containers»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"2.9 Hermetic tests
Заголовок раздела «2.9 Hermetic tests»Принципы:
- Нет сетевых вызовов к внешним сервисам (только
httptestили контейнеры). - Нет shared state — каждый тест строит/чистит данные сам.
- Нет реального времени — используем
clock.Mockили передаваемоеtime.Now. - Нет окружения — не читаем
os.Getenvбез mock’а. - Детерминизм —
math/randс фиксированным seed, отсортированные ключи map’ов.
// Плохо: непредсказуемоfunc TestRand(t *testing.T) { if rand.Intn(10) < 5 { t.Fatal("flaky") // иногда падает }}
// Хорошо: явный rand sourcefunc TestRand(t *testing.T) { r := rand.New(rand.NewSource(42)) assert.Equal(t, 6, r.Intn(10))}2.10 Golden files
Заголовок раздела «2.10 Golden files»Паттерн для проверки сложного вывода (JSON, HTML, генерируемый код):
testdata/ golden/ user_response.jsonvar 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 -update2.11 Snapshot testing (cupaloy)
Заголовок раздела «2.11 Snapshot testing (cupaloy)»cupaloy автоматизирует golden-файлы:
import "github.com/bradleyjkemp/cupaloy/v2"
func TestSnapshot(t *testing.T) { out := generate() cupaloy.SnapshotT(t, out) // создаст .snapshots/ при первом запуске}// UPDATE_SNAPSHOTS=true go test2.12 Coverage modes
Заголовок раздела «2.12 Coverage modes»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.outgo tool cover -func=cover.outGo 1.20+ integration coverage
Заголовок раздела «Go 1.20+ integration coverage»С Go 1.20 добавлена возможность собирать покрытие не только в go test, а из любого скомпилированного бинаря:
go build -cover -o myserver ./cmd/serverGOCOVERDIR=cov ./myserver &# ... интеграционные тесты, ходящие в myserver ...kill %1go tool covdata percent -i=covgo tool covdata textfmt -i=cov -o cov.outgo tool cover -html=cov.outЭто позволяет получать реальное покрытие, собранное в E2E.
2.13 BDD / Property / Property-based / Benchmark
Заголовок раздела «2.13 BDD / Property / Property-based / Benchmark»- 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для регрессий.
2.14 Test pyramid в Go контексте
Заголовок раздела «2.14 Test pyramid в Go контексте» ┌──────────────────────────┐ │ E2E (10%) │ testcontainers + HTTP ├──────────────────────────┤ │ Integration (20-30%) │ testcontainers, реальная БД ├──────────────────────────┤ │ Unit (60-70%) │ моки, чистые функции └──────────────────────────┘В Go пакет — естественная единица тестирования. Стремитесь к: пакет проверяется в одиночку без сети/диска, интеграция — отдельные тесты с //go:build integration тегом.
2.15 Flaky tests
Заголовок раздела «2.15 Flaky tests»Причины:
- Зависимость от времени (
time.Sleep, не передаваемоеclock). - Гонки данных (запустите
go test -race ./...). - Параллельные тесты делят shared state (общая БД-таблица).
- Сетевые тесты.
map-итерация (порядок недетерминированный).goroutineleak — предыдущий тест оставил активную goroutine, она шумит в следующих.
Лечение:
go test -count=100 ./pkgлокально для поиска редких падений.t.Parallel+ изоляция данных (t.TempDir, отдельный schema/db).- Inject зависимости (clock, randomness, env).
goleak.VerifyTestMainдля контроля утечек goroutine.
2.16 TestMain
Заголовок раздела «2.16 TestMain»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() }())}3. Gotchas
Заголовок раздела «3. Gotchas»3.1 t.Parallel и shared map
Заголовок раздела «3.1 t.Parallel и shared map»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 поймает.
3.2 Closure над переменной цикла (legacy <1.22)
Заголовок раздела «3.2 Closure над переменной цикла (legacy <1.22)»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 обязательно.
3.3 t.Cleanup внутри parallel subtest
Заголовок раздела «3.3 t.Cleanup внутри parallel subtest»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 доживёт }) }}3.4 defer вместо t.Cleanup с parallel
Заголовок раздела «3.4 defer вместо t.Cleanup с parallel»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 }) }}3.5 Go 1.20+ subtest fail propagation
Заголовок раздела «3.5 Go 1.20+ subtest fail propagation»t.Skipped(), t.Failed() родителя зависят от детей; если дочерний тест провалился, родитель тоже считается провалившимся. Это иногда удивляет в логике cleanup.
3.6 httptest и keep-alive
Заголовок раздела «3.6 httptest и keep-alive»httptest.Server использует http.Server с keep-alive. Если клиент держит соединение, srv.Close() может ждать. Используйте srv.CloseClientConnections() или явный Connection: close.
3.7 testify ассерты возвращают bool
Заголовок раздела «3.7 testify ассерты возвращают bool»ok := assert.NoError(t, err)if !ok { return // продолжать после фатального условия некорректно}assert.* возвращает bool, чтобы можно было ветвиться, но обычно лучше require.*.
3.8 gomock ordering
Заголовок раздела «3.8 gomock ordering»По умолчанию порядок вызовов не важен. Чтобы зафиксировать — gomock.InOrder(...) или .After():
gomock.InOrder( m.EXPECT().Connect(), m.EXPECT().SendData(), m.EXPECT().Disconnect(),)3.9 testcontainers и CI
Заголовок раздела «3.9 testcontainers и CI»В CI без Docker (например, Buildkite на host без daemon) testcontainers падает. Тег:
//go:build integrationИ в Makefile:
test: go test ./... -shorttest-integration: go test -tags=integration ./...3.10 Coverage и dead code
Заголовок раздела «3.10 Coverage и dead code»set/count режимы не учитывают код, который не достигается (по причине build tag, например). Используйте -coverpkg, чтобы охватить весь модуль.
3.11 -race замедляет тесты в 2-10x
Заголовок раздела «3.11 -race замедляет тесты в 2-10x»В CI пускайте -race отдельным job’ом, не блокируйте PR-проверки 30-минутным race-build’ом, если тестов очень много.
3.12 panic в goroutine внутри теста
Заголовок раздела «3.12 panic в goroutine внутри теста»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 }}()<-done3.13 leaking goroutines
Заголовок раздела «3.13 leaking goroutines»После теста горутина могла не завершиться. Используйте go.uber.org/goleak:
func TestMain(m *testing.M) { goleak.VerifyTestMain(m)}4. Производительность
Заголовок раздела «4. Производительность»4.1 Параллельность и -p, -parallel
Заголовок раздела «4.1 Параллельность и -p, -parallel»-p— параллелизм между пакетами (по умолчаниюGOMAXPROCS).-parallel— параллелизм внутри пакета (дляt.Parallel).
Для CI на 8 CPU при 50 пакетах:
go test -p=4 -parallel=4 ./...Снижает CPU starvation, особенно когда каждый пакет — отдельный процесс.
4.2 Skip dependencies build
Заголовок раздела «4.2 Skip dependencies build»go test -i ./... (-i устарел) или go build ./... отдельной фазой. В современных версиях кэш билдов делает это автоматически.
4.3 Кэширование результатов тестов
Заголовок раздела «4.3 Кэширование результатов тестов»Go кэширует успешные результаты по содержимому входов. Чтобы принудительно перепрогнать:
go clean -testcacheGOCACHE=$(mktemp -d) go test ./...4.4 Бенчмарки тестов
Заголовок раздела «4.4 Бенчмарки тестов»go test -bench=. -benchmem ./pkgЕсли тесты медленные:
- Профилирование:
go test -cpuprofile=cpu.out,go tool pprof cpu.out. - Перепиши setup в
TestMainилиsync.Once. - Используйте
t.Parallel.
4.5 testcontainers reuse
Заголовок раздела «4.5 testcontainers reuse»С WithReuse() контейнер не удаляется после тестов — повторный запуск стартует за секунды вместо 5-10. В CI обычно без reuse (изолированные runners).
4.6 In-memory vs реальная БД
Заголовок раздела «4.6 In-memory vs реальная БД»- Unit (in-memory) — sqlite в RAM, дешёво, но не покрывает PG-специфику.
- Integration (testcontainers) — реальный PG, дорого, но даёт уверенность.
Гибрид: 80% unit с in-memory + 20% integration через testcontainers.
4.7 Parallel + БД sharding
Заголовок раздела «4.7 Parallel + БД sharding»Параллельные тесты на одной БД ломаются. Решения:
- Отдельная схема на каждый тест (
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}4.8 Benchmarks в CI
Заголовок раздела «4.8 Benchmarks в CI»Регулярные бенчмарки + benchstat для сравнения main vs PR. Утилита: golang.org/x/perf/cmd/benchstat.
go test -bench=. -count=10 -run=^$ > pr.txtgit checkout maingo test -bench=. -count=10 -run=^$ > base.txtbenchstat base.txt pr.txt5. Вопросы на собеседовании
Заголовок раздела «5. Вопросы на собеседовании»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 помечает тест как провалившийся, но продолжает. Fatalf — Errorf + 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=./... — собирать покрытие не только текущего пакета, но и зависимостей. Полезно для интеграционных тестов.
6. Practice
Заголовок раздела «6. Practice»Задача 1: Table-driven + Parallel
Заголовок раздела «Задача 1: Table-driven + Parallel»Напишите функцию SumDigits(n int) int и тесты на 5+ кейсов с t.Run и t.Parallel. Используйте t.Cleanup для логирования времени теста.
Задача 2: Мок репозитория через gomock
Заголовок раздела «Задача 2: Мок репозитория через gomock»Дан интерфейс UserRepo { Get(ctx, id) (*User, error) }. Напишите UserService.Greet(id), генерируйте мок через mockgen, проверьте успешный и error-case.
Задача 3: httptest интеграция
Заголовок раздела «Задача 3: httptest интеграция»Напишите HTTP-клиент Weather.Get(city) (GET /weather?city=). Тестом проверьте через httptest.NewServer, что клиент корректно парсит JSON и обрабатывает 5xx.
Задача 4: testcontainers + PostgreSQL
Заголовок раздела «Задача 4: testcontainers + PostgreSQL»Поднимите Postgres через testcontainers, накатайте миграцию (создаёт users), вставьте 3 строки, прочитайте, проверьте.
Задача 5: Golden file
Заголовок раздела «Задача 5: Golden file»Напишите RenderReport(items []Item) []byte (JSON или CSV). Сохраните ожидаемый вывод в testdata/golden/report.json и сравните в тесте.
Задача 6: Coverage и -coverpkg
Заголовок раздела «Задача 6: Coverage и -coverpkg»Создайте 2 пакета: pkg/a использует pkg/b. Тест в pkg/a. Запустите с -coverpkg=./... и подтвердите, что покрытие pkg/b ненулевое.
Задача 7: testify suite
Заголовок раздела «Задача 7: testify suite»Реализуйте OrderSuite с SetupSuite/SetupTest/TearDownSuite для тестирования OrderRepo. Минимум 3 теста.
Задача 8: Goleak
Заголовок раздела «Задача 8: Goleak»Добавьте TestMain с goleak.VerifyTestMain(m). Намеренно запустите goroutine без завершения — проверьте, что тест падает.
7. Источники
Заголовок раздела «7. Источники»- Документация testing: https://pkg.go.dev/testing (актуально для Go 1.22+).
- Go Blog “Testing in Go: clean tests using t.Cleanup”: https://go.dev/blog/subtests.
- testify: https://github.com/stretchr/testify — issues и README.
- uber-go/mock: https://github.com/uber-go/mock — README, миграция с golang/mock.
- mockery v2: https://vektra.github.io/mockery/.
- testcontainers-go: https://golang.testcontainers.org/.
- Go 1.20 release notes (coverage): https://tip.golang.org/doc/go1.20#cover.
- Go 1.22 loop var change: https://go.dev/blog/loopvar-preview.
- rapid (property-based): https://pkg.go.dev/pgregory.net/rapid.
- goleak: https://github.com/uber-go/goleak.
- Книга “Learning Go” Jon Bodner (2nd ed., 2024) — глава о тестировании.