Тестирование в Go: unit, integration, e2e от А до Я

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

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

Начать курс

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

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

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

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

Unit-тесты проверяют работу отдельных функций или методов в изоляции от внешних зависимостей. В Go для этого используется стандартный пакет testing. Основные правила: файлы тестов должны называться *_test.go, а функции тестов — начинаться с Test.

1.1. Простой 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 ./... или go test -v для подробного вывода.

1.2. Table-driven тесты (табличные тесты)

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

func TestAddTableDriven(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)
            }
        })
    }
}

Такой подход делает тесты легко читаемыми и расширяемыми.

1.3. Тестирование с ошибками

Для функций, возвращающих ошибки, используйте t.Fatal или t.Errorf:

func TestDivide(t *testing.T) {
    _, err := Divide(10, 0)
    if err == nil {
        t.Fatal("expected error for division by zero")
    }
}

2. Интеграционное тестирование: работа с зависимостями

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

2.1. Использование build-тегов

Создайте файл с тегом //go:build integration в начале:

//go:build integration

package db

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

func TestPostgresConnection(t *testing.T) {
    dsn := "host=localhost port=5432 user=test password=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()
    
    if err := db.Ping(); err != nil {
        t.Fatalf("failed to ping db: %v", err)
    }
}

Запуск только интеграционных тестов: go test -tags=integration ./...

2.2. Моки и заглушки

Для изоляции тестов используйте интерфейсы и моки. Пример с популярной библиотекой gomock:

// user.go
type UserRepository interface {
    GetUser(id int) (*User, error)
}

type UserService struct {
    repo UserRepository
}

func (s *UserService) GetUserName(id int) (string, error) {
    user, err := s.repo.GetUser(id)
    if err != nil {
        return "", err
    }
    return user.Name, nil
}
// user_test.go (с использованием gomock)
func TestGetUserName(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()
    
    mockRepo := NewMockUserRepository(ctrl)
    mockRepo.EXPECT().GetUser(1).Return(&User{Name: "Alice"}, nil)
    
    service := &UserService{repo: mockRepo}
    name, err := service.GetUserName(1)
    if err != nil || name != "Alice" {
        t.Errorf("unexpected result: %s, err: %v", name, err)
    }
}

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

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

3.1. HTTP-тесты с httptest

Стандартный пакет net/http/httptest позволяет тестировать HTTP-обработчики без запуска реального сервера:

func TestUserHandler(t *testing.T) {
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"name":"Alice"}`))
    })
    
    req := httptest.NewRequest("GET", "/user/1", nil)
    rec := httptest.NewRecorder()
    handler.ServeHTTP(rec, req)
    
    if rec.Code != http.StatusOK {
        t.Errorf("expected 200, got %d", rec.Code)
    }
    
    expected := `{"name":"Alice"}`
    if rec.Body.String() != expected {
        t.Errorf("expected %s, got %s", expected, rec.Body.String())
    }
}

3.2. Запуск тестовой инфраструктуры

Для полноценных e2e-тестов с БД и сервисами используйте testcontainers-go:

import (
    "context"
    "testing"
    "github.com/testcontainers/testcontainers                

Блоги

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