← Назад к вопросам

Как тестировать код в Go? Какие инструменты используете?

2.0 Middle🔥 161 комментариев
#Тестирование

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI4 апр. 2026 г.

Ответ сгенерирован нейросетью и может содержать ошибки

Тестирование в Go: философия и инструменты

Тестирование в Go — это не просто дополнительная опция, а интегральная часть языка и его философии. Go поставляется с мощным тестовым фреймворком прямо из коробки, что делает написание тестов стандартной практикой для любого проекта.

Встроенные средства тестирования

Базовый фреймворк testing

Пакет testing предоставляет все необходимое для unit-тестирования. Тестовые файлы должны заканчиваться на _test.go и содержать функции, начинающиеся с Test:

package calculator

import "testing"

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

// Табличные тесты (table-driven tests) — рекомендуемый подход
func TestSubtract(t *testing.T) {
    testCases := []struct {
        a, b     int
        expected int
    }{
        {5, 3, 2},
        {0, 0, 0},
        {-5, -3, -2},
        {10, 15, -5},
    }
    
    for _, tc := range testCases {
        result := Subtract(tc.a, tc.b)
        if result != tc.expected {
            t.Errorf("Subtract(%d, %d) = %d; want %d", 
                tc.a, tc.b, result, tc.expected)
        }
    }
}

Запуск тестов

# Все тесты в пакете
go test ./...

# С детальным выводом
go test -v ./...

# С покрытием кода
go test -cover ./...

# Генерация HTML отчета о покрытии
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html

Тестирование main-пакетов

Для тестирования main-пакетов я создаю отдельный тестовый файл и использую субтесты:

func TestMainFunction(t *testing.T) {
    tests := []struct {
        name string
        args []string
        want int
    }{
        {"no args", []string{}, 1},
        {"help", []string{"-help"}, 0},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // Сохраняем и восстанавливаем os.Args
            oldArgs := os.Args
            defer func() { os.Args = oldArgs }()
            
            os.Args = append([]string{"cmd"}, tt.args...)
            
            // Используем буфер для захвата вывода
            var buf bytes.Buffer
            exitCode := realMain(&buf)
            
            if exitCode != tt.want {
                t.Errorf("exit code = %d, want %d", exitCode, tt.want)
            }
        })
    }
}

Продвинутые инструменты и техники

Моки и стабы

Для изоляции тестируемых компонентов я использую:

  1. Интерфейсы — ключевой паттерн для тестируемости
  2. gomock/mockgen — генерация моков на основе интерфейсов
  3. Ручные заглушки — простые реализации для тестов
// Определяем интерфейс
type Storage interface {
    GetUser(id int) (*User, error)
    SaveUser(user *User) error
}

// Тестовая реализация
type MockStorage struct {
    users map[int]*User
    err   error
}

func (m *MockStorage) GetUser(id int) (*User, error) {
    if m.err != nil {
        return nil, m.err
    }
    return m.users[id], nil
}

// В тесте используем mock
func TestUserService(t *testing.T) {
    mockStorage := &MockStorage{
        users: map[int]*User{1: {ID: 1, Name: "John"}},
    }
    
    service := NewUserService(mockStorage)
    user, err := service.GetUser(1)
    
    // assertions
}

Интеграционные и e2e тесты

Для разных уровней тестирования:

  1. testcontainers-go — для поднятия реальных зависимостей (БД, кэши)
  2. Кастомные test helpers — утилиты для настройки тестового окружения
  3. Отдельные теговые группы — использование build tags для разделения тестов
// +build integration

package integration

func TestDatabaseIntegration(t *testing.T) {
    ctx := context.Background()
    
    // Запуск PostgreSQL в контейнере
    container, err := testcontainers.StartPostgreSQLContainer(ctx)
    if err != nil {
        t.Fatal(err)
    }
    defer container.Terminate(ctx)
    
    // Получение DSN и подключение
    dsn := container.GetDSN()
    db, err := sql.Open("postgres", dsn)
    
    // Тестовые сценарии с реальной БД
}

Параллельное выполнение

Go отлично поддерживает параллельные тесты:

func TestParallelFeatures(t *testing.T) {
    t.Run("group", func(t *testing.T) {
        t.Run("test1", func(t *testing.T) {
            t.Parallel()
            // длительная операция 1
        })
        
        t.Run("test2", func(t *testing.T) {
            t.Parallel()
            // длительная операция 2
        })
    })
}

Дополнительные инструменты в моём стеке

1. Генерация тестовых данных

  • go-fuzz — для фаззинга и поиска краевых случаев
  • testfixtures — загрузка фикстур в БД
  • Кастомные factory-функции — для создания тестовых объектов

2. Проверка качества

  • golangci-lint — линтер с множеством анализаторов
  • staticcheck — статический анализ
  • errcheck — проверка обработки ошибок

3. Бенчмаркинг

Встроенная поддержка бенчмарков — мощный инструмент:

func BenchmarkCalculate(b *testing.B) {
    data := prepareTestData()
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        Calculate(data)
    }
}

// Запуск: go test -bench=. -benchmem

4. Продвинутые фреймворки

Хотя встроенного testing часто достаточно, иногда использую:

  • testify — для assertions и моков (хотя предпочитаю минимализм)
  • ginkgo/gomega — BDD-стиль для сложных проектов
  • httptest — для тестирования HTTP-обработчиков

Практики и рекомендации

  1. Тестовая структура — mirror структуры основного кода
  2. Именование тестовTest{Unit}_{Scenario}_{Expected}
  3. Чистые тесты — каждый тест независим, нет глобального состояния
  4. Покрытие — стремиться к 70-80% осмысленного покрытия, не гнаться за 100%
  5. Интеграционные тесты — в отдельных пакетах с тегом integration
  6. Тестовые данные — использовать testdata/ директорию

CI/CD интеграция

В pipeline всегда включаю:

# .github/workflows/test.yml
steps:
  - run: go test ./... -race -coverprofile=coverage.out
  - run: go tool cover -func=coverage.out
  - run: golangci-lint run ./...

Ключевой принцип: тесты в Go должны быть простыми, быстрыми и полезными. Избыточные моки и сложные фреймворки часто приносят больше вреда, чем пользы. Я начинаю со встроенных средств и добавляю дополнительные инструменты только при явной необходимости.

Как тестировать код в Go? Какие инструменты используете? | PrepBro