Тестирование в Go: unit, integration, e2e — полное руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему тестирование в Go — это важно?

Go (Golang) — это язык, который с самого проектирования ориентирован на простоту, производительность и надежность. Встроенный пакет testing и мощные инструменты, такие как go test, делают процесс написания тестов неотъемлемой частью разработки. Без качественного тестирования даже самый элегантный код со временем превращается в "хрупкую" систему, где одно изменение ломает всё.

В этой статье мы разберем три ключевых уровня тестирования: unit (модульное), integration (интеграционное) и e2e (end-to-end). Вы узнаете, чем они отличаются, когда их применять и как писать эффективные тесты на Go. Материал будет полезен как новичкам, так и опытным разработчикам, желающим систематизировать знания.

1. Unit-тестирование (Модульное тестирование)

Unit-тесты — это фундамент пирамиды тестирования. Они проверяют работу отдельных функций, методов или модулей изолированно от внешних зависимостей (базы данных, сети, файловой системы).

Основные принципы unit-тестов в Go

  • Изоляция: Тестируемый код не должен зависеть от внешних сервисов.
  • Быстрота: Unit-тесты выполняются за миллисекунды.
  • Детерминированность: Результат теста не должен меняться от запуска к запуску.

Пример простого unit-теста

Допустим, у нас есть функция для сложения двух чисел:

// math.go
package math

func Add(a, b int) int {
    return a + b
}

Тест для этой функции будет выглядеть так:

// math_test.go
package math

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)
    }
}

Запускается тест командой: go test ./...

Табличные тесты (Table-driven tests)

Go-сообщество предпочитает табличные тесты для проверки множества кейсов:

func TestAddTable(t *testing.T) {
    tests := []struct {
        a, b, expected int
    }{
        {1, 1, 2},
        {0, 0, 0},
        {-1, 1, 0},
        {100, 200, 300},
    }

    for _, tt := range tests {
        result := Add(tt.a, tt.b)
        if result != tt.expected {
            t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected)
        }
    }
}

Совет: Используйте t.Run() для создания вложенных тестов с понятными именами — это улучшает читаемость отчетов.

2. Интеграционное тестирование

Интеграционные тесты проверяют взаимодействие между компонентами системы: база данных, API, внешние сервисы. В отличие от unit-тестов, здесь мы поднимаем реальные (или почти реальные) зависимости.

Особенности интеграционных тестов в Go

  • Часто требуют настройки окружения (например, тестовой БД).
  • Могут использовать build tags для разделения unit и integration тестов.
  • Выполняются медленнее, поэтому их запускают отдельно.

Пример: тестирование работы с БД (PostgreSQL)

Создадим файл с build tag integration:

// db_test.go
//go:build integration

package db

import (
    "database/sql"
    "testing"
)

func TestSaveUser(t *testing.T) {
    // Подключаемся к тестовой БД
    dsn := "host=localhost user=test password=test dbname=testdb sslmode=disable"
    db, err := sql.Open("postgres", dsn)
    if err != nil {
        t.Fatalf("failed to connect to db: %v", err)
    }
    defer db.Close()

    // Очищаем данные перед тестом
    _, _ = db.Exec("TRUNCATE TABLE users")

    // Выполняем тестируемую операцию
    user := User{Name: "Alice", Email: "alice@example.com"}
    err = SaveUser(db, user)
    if err != nil {
        t.Errorf("SaveUser() error = %v", err)
    }

    // Проверяем, что пользователь действительно сохранен
    var count int
    db.QueryRow("SELECT COUNT(*) FROM users WHERE email = $1", user.Email).Scan(&count)
    if count != 1 {
        t.Errorf("expected 1 user, got %d", count)
    }
}

Запуск: go test -tags=integration ./...

Использование моков и тестовых контейнеров

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

  • Mock-объекты — библиотеки gomock или testify/mock.
  • Testcontainers — для поднятия временных БД, Redis и т.д. в Docker.

Пример с testify/mock:

type MockDB struct {
    mock.Mock
}

func (m *MockDB) GetUser(id int) (User, error) {
    args := m.Called(id)
    return args.Get(0).(User), args.Error(1)
}

3. E2E (End-to-End) тестирование

E2E-тесты проверяют систему целиком, имитируя действия реального пользователя. Они запускаются в окружении, максимально приближенном к production.

Когда нужны e2e-тесты?

  • Проверка критических пользовательских сценариев (регистрация, оплата).
  • Тестирование интеграции нескольких микросервисов.
  • Валидация API Gateway, балансировщиков нагрузки.

Пример e2e-теста для HTTP API

Используем пакет net/http/httptest для создания тестового сервера:

package e2e

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestUserRegistrationFlow(t *testing.T) {
    // Создаем тестовый сервер (имитация полного приложения)
    handler := setupRouter() // ваша функция инициализации роутера
    ts := httptest.NewServer                

Блоги

Рекомендаций по книгам