> Как был организован процесс тестирования (Go)

Уровень: senior · Роль: backend · Категория: Технические вопросы

Компании: Orion_soft

Стек: Go

> Пример ответа

Короткий ответ

Процесс тестирования в Go-проектах строится вокруг встроенного пакета testing, табличных тестов и моков через интерфейсы. Основные уровни: unit-тесты для бизнес-логики, integration-тесты с реальными зависимостями (БД, внешние API), e2e-тесты через HTTP-хендлеры. Обязательны go test -race, покрытие через -coverprofile, а также тесты для конкурентных сценариев. В CI тесты запускаются на каждый PR, с отдельным этапом для линтеров и статического анализа.

Подробное объяснение

Процесс тестирования в Go-проектах обычно включает несколько уровней:

  1. Unit-тесты - покрывают отдельные функции и методы. Используют табличный подход: массив структур с входными данными и ожидаемым результатом. Для изоляции зависимостей применяют интерфейсы и моки (gomock, testify/mock, или ручные моки).

  2. Integration-тесты - проверяют взаимодействие с реальными компонентами: БД (testcontainers, dockertest), Redis, внешние сервисы. Обычно помечаются build-тегами (//go:build integration) или отдельными пакетами, чтобы не замедлять обычный прогон.

  3. HTTP-тесты - через httptest.NewServer или httptest.NewRecorder проверяют хендлеры, middleware, валидацию запросов, коды ответов, формат JSON.

  4. Конкурентные тесты - обязательны для Go: go test -race выявляет data race. Также пишутся тесты с sync.WaitGroup, каналами, проверкой отсутствия deadlock.

  5. E2E-тесты - запускаются отдельно, часто в CI после деплоя в staging. Могут использовать docker-compose для поднятия всего окружения.

Организация файлов: тесты лежат рядом с кодом (foo.go → foo_test.go). Для вспомогательных функций - testutils пакет. Фикстуры - в testdata/. Покрытие измеряется через go test -coverprofile=coverage.out и анализируется в CI (порог, например, 80%).

В CI процесс выглядит так: go vet, golangci-lint, go test -race ./..., затем go test -coverprofile, затем сборка и деплой. Для интеграционных тестов - отдельный job с поднятием инфраструктуры.

На практике

В реальном проекте процесс выглядит так:

  • Каждый PR обязан содержать тесты на новую логику. Без этого ревью не проходит.
  • Для работы с БД используется sqlmock для unit-тестов и testcontainers для integration.
  • Для HTTP-хендлеров - httptest, проверяются статус-коды, заголовки, тело ответа.
  • Для конкурентных компонентов (воркеры, пулы) - обязательный -race.
  • Тесты на бизнес-логику пишутся в первую очередь, до реализации (TDD, если это принято в команде).
  • В CI настроен порог покрытия, но он не жесткий - анализируется динамика, а не абсолютное значение.

Пример типичного процесса: разработчик пишет код, затем тесты, запускает локально go test ./... -race, проверяет покрытие, затем пушит. CI дополнительно гоняет линтеры и интеграционные тесты. При падении - PR блокируется.

Пример кода

GO
// handler_test.go
func TestCreateUser(t *testing.T) {
tests := []struct{
name string
body string
wantStatus int
wantErr string
}{
{
name: "valid request",
body: `{"name":"Alice","email":"alice@example.com"}`,
wantStatus: http.StatusCreated,
},
{
name: "invalid email",
body: `{"name":"Bob","email":"not-an-email"}`,
wantStatus: http.StatusBadRequest,
wantErr: "invalid email",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(tt.body))
rec := httptest.NewRecorder()
handler := NewUserHandler(mockUserService{})
handler.CreateUser(rec, req)
if rec.Code != tt.wantStatus {
t.Errorf("status = %d, want %d", rec.Code, tt.wantStatus)
}
})
}
}
// Конкурентный тест
func TestWorkerPool(t *testing.T) {
var wg sync.WaitGroup
results := make(chan int, 10)
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
results <- n * 2
}(i)
}
wg.Wait()
close(results)
count := 0
for range results {
count++
}
if count != 10 {
t.Errorf("got %d results, want 10", count)
}
}

Как отвечать на собеседовании

Начни с общего описания уровней тестирования, затем переходи к конкретике Go: testing пакет, табличные тесты, -race, httptest. Обязательно упомяни моки через интерфейсы - это ключевая особенность Go-подхода. Расскажи про организацию в CI: линтеры, покрытие, интеграционные тесты отдельным этапом. Приведи пример из реального проекта: как решал проблему с нестабильными тестами, как обеспечивал изоляцию от внешних зависимостей. Покажи понимание trade-off: unit-тесты быстрые, но не ловят проблемы интеграции; e2e - медленные, но дают уверенность. Если спросят про покрытие - скажи, что гнался не за процентом, а за покрытием критических путей.

Что проверяет интервьюер

Интервьюер оценивает:

  • понимание уровней тестирования и их назначения;
  • знание идиоматичных Go-подходов: табличные тесты, интерфейсы для моков, httptest;
  • умение писать конкурентные тесты и использовать -race;
  • опыт с интеграционными тестами и инфраструктурой (testcontainers, docker);
  • понимание роли тестов в CI/CD и процессе разработки;
  • способность объяснить, как тестировать сложные сценарии: retry, timeout, распределенные системы.

Типичные ошибки

  • Тестирование только happy path, без ошибок и граничных случаев.
  • Использование глобального состояния в тестах - параллельные тесты начинают влиять друг на друга.
  • Отсутствие t.Run для подтестов - при падении непонятно, какой кейс упал.
  • Игнорирование -race - конкурентные баги не ловятся.
  • Моки через глобальные переменные вместо интерфейсов - сложно поддерживать.
  • Интеграционные тесты, которые зависят от локального окружения разработчика, а не поднимаются в CI.
  • Проверка только статус-кода без проверки тела ответа.
  • Тесты, которые проходят, но не проверяют ничего полезного (пустые assert).

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью