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

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

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

Начать курс

Введение: Почему продвинутое тестирование — это must have в Go

Go (Golang) славится своей простотой и встроенными инструментами для тестирования. Однако, когда проект вырастает за рамки "Hello, World", стандартный go test перестаёт быть достаточным. На продвинутом уровне разработчики сталкиваются с вопросами: как правильно изолировать тесты, как тестировать работу с базой данных, как имитировать внешние сервисы и как организовать сквозную проверку всего приложения.

В этой статье мы разберём три уровня тестирования — Unit, Integration и E2E — на примерах из реальной практики. Вы узнаете, как использовать моки, контейнеры для интеграционных тестов и как построить надёжный pipeline для CI/CD. Материал рассчитан на разработчиков, которые уже знакомы с основами Go и хотят поднять качество своего кода на новый уровень.

1. Unit-тесты: изоляция, моки и паттерн Table-Driven Tests

Unit-тесты проверяют работу одного модуля (функции, метода) в полной изоляции от внешних зависимостей. В Go для этого часто используют интерфейсы и библиотеки-мокеры, например testify/mock или gomock.

1.1. Интерфейсы как основа тестирования

Чтобы заменить реальный сервис на мок, код должен зависеть от интерфейса, а не от конкретной реализации. Рассмотрим пример сервиса для отправки уведомлений.

// notifier.go
type Notifier interface {
    Send(to string, message string) error
}

type UserService struct {
    Notifier Notifier
}

func (s *UserService) NotifyUser(userID string) error {
    // логика получения email пользователя
    email := userID + "@example.com"
    return s.Notifier.Send(email, "Welcome!")
}

1.2. Создание мока с testify/mock

Установим библиотеку: go get github.com/stretchr/testify.

// notifier_mock.go (в пакете user_test)
type NotifierMock struct {
    mock.Mock
}

func (m *NotifierMock) Send(to string, message string) error {
    args := m.Called(to, message)
    return args.Error(0)
}

1.3. Table-Driven Tests

Это стандартный паттерн в Go для тестирования множества кейсов. Он улучшает читаемость и упрощает добавление новых тестов.

// user_service_test.go
func TestNotifyUser(t *testing.T) {
    tests := []struct {
        name      string
        userID    string
        mockError error
        expectErr bool
    }{
        {"success", "user1", nil, false},
        {"fail_send", "user2", errors.New("send error"), true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            mockNotifier := new(NotifierMock)
            service := &UserService{Notifier: mockNotifier}

            mockNotifier.On("Send", tt.userID+"@example.com", "Welcome!").Return(tt.mockError)

            err := service.NotifyUser(tt.userID)
            if tt.expectErr {
                assert.Error(t, err)
            } else {
                assert.NoError(t, err)
            }
            mockNotifier.AssertExpectations(t)
        })
    }
}

Best practice: Используйте t.Run для субтестов — это позволяет запускать каждый кейс отдельно и видеть, какой именно упал.

2. Интеграционные тесты: работа с БД и внешними сервисами

Интеграционные тесты проверяют взаимодействие нескольких компонентов: Go-кода с базой данных, API-сервера с кэшем и т.д. Главный вызов — управлять состоянием окружения. Здесь на помощь приходят Testcontainers.

2.1. Testcontainers для Go

Testcontainers позволяют поднимать Docker-контейнеры прямо из тестов. Это даёт чистую, изолированную среду для каждого запуска.

Установка: go get github.com/testcontainers/testcontainers-go

2.2. Пример: тестирование репозитория с PostgreSQL

// repo_test.go
func TestUserRepository_GetByID(t *testing.T) {
    ctx := context.Background()

    // 1. Поднимаем PostgreSQL контейнер
    pgContainer, err := postgres.RunContainer(ctx,
        testcontainers.WithImage("postgres:16-alpine"),
        postgres.WithDatabase("testdb"),
        postgres.WithUsername("test"),
        postgres.WithPassword("test"),
    )
    require.NoError(t, err)
    defer pgContainer.Terminate(ctx)

    // 2. Получаем адрес подключения
    connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
    require.NoError(t, err)

    // 3. Инициализируем БД (миграции)
    db, err := sql.Open("postgres", connStr)
    require.NoError(t, err)
    defer db.Close()

    _, err = db.Exec(`CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)`)
    require.NoError(t, err)

    // 4. Вставляем тестовые данные
    _, err = db.Exec(`INSERT INTO users (name) VALUES ('Alice')`)
    require.NoError(t, err)

    // 5. Тестируем репозиторий
    repo := NewUserRepository(db)
    user, err := repo.GetByID(ctx, 1)
    require.NoError(t, err)
    assert.Equal(t, "Alice", user.Name)
}

2.3. Флаги для разделения тестов

Unit-тесты должны быть быстрыми. Интеграционные — медленнее. Используйте build tags или флаги, чтобы разделять их.

// file: repo_test.go
//go:build integration

package repository_test

// ... тесты выше

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

3. E2E-тесты: сквозная проверка HTTP API

End-to-end (E2E) тесты проверяют приложение целиком, как его видит пользователь. В веб-сервисах на Go это означает отправку HTTP-запросов к запущенному серверу и проверку ответов.

3.1. Запуск тестового сервера

Лучше не тестировать против продакшена, а поднимать отдельный экземпляр приложения. Используйте httptest.NewServer для быстрого старта.

// e2e_test.go
func TestE2E_CreateUser(t *testing.T) {
    // Поднимаем тестовый сервер
    handler := setupRouter() // ваша функция с роутами
    ts := httptest.NewServer(handler)
    defer ts.Close()

    // Создаём пользователя через API
    requestBody := `{"name":"Bob","                

Блоги

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