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

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

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

Начать курс

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

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

1. Unit-тесты в Go: основа основ

Unit-тесты проверяют отдельные функции или методы в изоляции. В Go для этого достаточно создать файл с суффиксом _test.go и написать функции вида TestXxx(t *testing.T).

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

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

// math.go
package main

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

Тест для неё:

// math_test.go
package main

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)

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

func TestAddTable(t *testing.T) {
    tests := []struct {
        name string
        a, b int
        want int
    }{
        {"positive", 2, 3, 5},
        {"negative", -1, -2, -3},
        {"zero", 0, 0, 0},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := Add(tt.a, tt.b); got != tt.want {
                t.Errorf("Add() = %d, want %d", got, tt.want)
            }
        })
    }
}

Запускаются unit-тесты командой:

go test ./...

2. Интеграционные тесты: проверяем взаимодействие

Интеграционные тесты проверяют, как разные части системы работают вместе: база данных, API, файловая система. В Go их принято выносить в отдельные файлы с тегом сборки или размещать в пакете integration.

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

Создайте файл integration_test.go с тегом:

//go:build integration

package integration

import (
    "database/sql"
    "testing"
    _ "github.com/lib/pq"
)

func TestDatabaseConnection(t *testing.T) {
    dsn := "host=localhost port=5432 user=test dbname=test sslmode=disable"
    db, err := sql.Open("postgres", dsn)
    if err != nil {
        t.Fatalf("Failed to open DB: %v", err)
    }
    defer db.Close()

    err = db.Ping()
    if err != nil {
        t.Fatalf("Failed to ping DB: %v", err)
    }
}

Запуск только интеграционных тестов:

go test -tags=integration ./...

Работа с тестовой БД

Для интеграционных тестов часто используют testcontainers-go или in-memory БД (например, SQLite). Пример с testcontainers:

func TestWithPostgresContainer(t *testing.T) {
    ctx := context.Background()
    pgContainer, err := postgres.RunContainer(ctx,
        testcontainers.WithImage("postgres:15"),
        postgres.WithDatabase("testdb"),
    )
    require.NoError(t, err)
    defer pgContainer.Terminate(ctx)

    dsn, _ := pgContainer.ConnectionString(ctx)
    // используем dsn для подключения
}

3. E2E-тесты: проверка системы целиком

End-to-End тесты симулируют поведение пользователя и проверяют всю систему от начала до конца. В Go их часто пишут для HTTP-сервисов с использованием пакета net/http/httptest.

Пример E2E-теста для REST API

func TestCreateUserE2E(t *testing.T) {
    // Запускаем тестовый сервер
    router := setupRouter() // ваша функция инициализации роутера
    ts := httptest.NewServer(router)
    defer ts.Close()

    // Отправляем запрос
    payload := `{"name":"John","email":"john@example.com"}`
    resp, err := http.Post(ts.URL+"/users", "application/json",
        strings.NewReader(payload))
    if err != nil {
        t.Fatal(err)
    }
    defer resp.Body.Close()

    // Проверяем статус
    if resp.StatusCode != http.StatusCreated {
        t.Errorf("Expected status 201, got %d", resp.StatusCode)
    }
}

Мокирование внешних сервисов

Для E2E-тестов часто нужно изолировать внешние зависимости. Используйте библиотеку gomock или testify/mock:

type MockPaymentService struct {
    mock.Mock
}

func (m *MockPaymentService) Charge(amount float64) error {
    args := m.Called(amount)
    return args.Error(0)
}

func TestOrderWithMock(t *testing.T) {
    mockSvc := new(MockPaymentService)
    mockSvc.On("Charge", 100.0).Return(nil)

    order := NewOrder(mockSvc)
    err := order.Process(100.0)
    assert.NoError(t, err)
    mockSvc.AssertExpectations(t)
}

4. Best Practices и полезные инструменты

  • Именование: используйте осмысленные имена тестов, например TestUserCreate_ValidInput_Success.
  • Изоляция: каждый тест должен быть независимым. Используйте t.Parallel() для ускорения.
  • Покрытие: запускайте go test -cover, чтобы видеть процент покрытия кода.
  • Фикстуры: для сложных данных используйте testdata папку или библиотеку go-faker.
  • CI/CD: интеграционные и E2E-тесты запускайте только в CI-пайплайне, а unit-тесты — при каждом коммите.

Популярные библиотеки для тестирования в Go:

  • testify — assert, mock, suite

Блоги

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