Изоляция без боли: Mock и фикстуры в Go тестах

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

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

Начать курс

Когда реальность мешает: зачем нам моки и фикстуры в Go?

Представьте, что вы тестируете функцию, которая отправляет email-уведомление. Каждый запуск теста — это реальное письмо в почтовый ящик. Через десять тестов ваш ящик забит, а коллеги проклинают спам. Или хуже: функция обращается к внешнему API, которое платное или работает раз в сутки.

В мире Go (или Golang, как его часто называют) тестирование — это не просто галочка в чек-листе. Это фундамент надежности. Но чтобы тесты были быстрыми и независимыми, нам нужно изолировать код от внешнего мира. Именно здесь на сцену выходят mock-объекты (моки) и фикстуры (fixtures).

В этой статье мы разберем, как правильно подменять зависимости (мокать) и готовить данные (фикстуры) в Go. Вы узнаете, как писать тесты, которые не падают из-за сети, базы данных или времени суток. Поехали!

Что такое Mock и Fixture в контексте Go?

Mock-объекты: двойники для сложных систем

Mock-объекты — это программные "заглушки", которые имитируют поведение реальных компонентов. Вместо того чтобы вызывать настоящую базу данных или HTTP-сервис, мы подставляем объект, который возвращает заранее заданные ответы.

В Go моки особенно важны, потому что язык поощряет работу с интерфейсами. Если ваш код зависит от интерфейса, вы легко можете подменить реализацию на тестовую.

Фикстуры: подготовка сцены

Фикстуры (Fixtures) — это данные или состояние, которые должны быть установлены до начала теста. Например, создание временной базы данных, заполнение её тестовыми записями или настройка файловой системы.

В отличие от моков, которые подменяют поведение, фикстуры подготавливают окружение. Они отвечают на вопрос: "Что должно быть готово, чтобы тест мог стартовать?"

Пишем моки в Go: от простого к сложному

Самый простой способ: ручная реализация интерфейса

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

Допустим, у нас есть интерфейс для отправки уведомлений:

package notifier

type Notifier interface {
    Send(to string, message string) error
}

// Реальная реализация (не для тестов)
type EmailNotifier struct{}

func (e *EmailNotifier) Send(to string, message string) error {
    // Отправка реального email
    return nil
}

В тестовом файле мы создаем мок:

package notifier

import "testing"

type MockNotifier struct {
    Called bool
    LastTo string
    LastMsg string
}

func (m *MockNotifier) Send(to string, msg string) error {
    m.Called = true
    m.LastTo = to
    m.LastMsg = msg
    return nil
}

func TestSendAlert(t *testing.T) {
    mock := &MockNotifier{}
    // Используем mock в функции, которая принимает Notifier
    SendAlert(mock, "user@test.com", "Hello!")
    
    if !mock.Called {
        t.Error("Метод Send не был вызван")
    }
    if mock.LastTo != "user@test.com" {
        t.Errorf("Неверный получатель: %s", mock.LastTo)
    }
}

Плюсы: полный контроль, никаких зависимостей.
Минусы: много шаблонного кода для сложных интерфейсов.

Используем testify/mock: мощно и удобно

Библиотека testify — стандарт де-факто для моков в Go. Она предоставляет гибкий фреймворк для создания моков с проверкой аргументов и количества вызовов.

Установка:

go get github.com/stretchr/testify

Пример с использованием testify/mock:

package user

import (
    "testing"
    "github.com/stretchr/testify/mock"
)

// Определяем интерфейс
type UserRepository interface {
    GetUser(id int) (string, error)
}

// Создаем мок-структуру
type MockUserRepo struct {
    mock.Mock
}

func (m *MockUserRepo) GetUser(id int) (string, error) {
    args := m.Called(id)
    return args.String(0), args.Error(1)
}

func TestGetUserName(t *testing.T) {
    repo := new(MockUserRepo)
    
    // Настраиваем поведение мока
    repo.On("GetUser", 42).Return("John Doe", nil)
    
    name, err := GetUserName(repo, 42)
    
    if err != nil {
        t.Fatal(err)
    }
    if name != "John Doe" {
        t.Errorf("Ожидалось John Doe, получено %s", name)
    }
    
    // Проверяем, что метод был вызван именно с аргументом 42
    repo.AssertExpectations(t)
}

testify позволяет проверять последовательность вызовов, возвращать разные значения при повторных вызовах и даже имитировать панику.

Mock для HTTP-запросов: httptest

Когда ваш код делает внешние HTTP-вызовы, не нужно поднимать реальный сервер. Стандартная библиотека Go предлагает пакет net/http/httptest.

package api

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

func TestFetchData(t *testing.T) {
    // Создаем тестовый сервер
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"status": "ok"}`))
    }))
    defer server.Close()
    
    // Передаем URL тестового сервера в нашу функцию
    result, err := FetchData(server.URL)
    if err != nil {
        t.Fatal(err)
    }
    if result != "ok" {
        t.Errorf("Ожидалось 'ok', получено '%s'", result)
    }
}

httptest — это "родной" инструмент Go, который не требует сторонних библиотек. Он идеально подходит для тестирования клиентов к внешним API.

Фикстуры в Go: готовим данные для тестов

Временные файлы и директории

Если ваш код работает с файловой системой, используйте t.TempDir(). Начиная с Go 1.15, эта функция создает временную директорию, которая автоматически удаляется после теста.

func TestProcessFile(t *testing.T) {
    dir := t.TempDir()
    filePath := dir + "/test.txt"
    
    // Создаем фикстуру — файл с тестовыми данными
    os.WriteFile(filePath, []byte("hello world"), 0644)
    
    result := ProcessFile(filePath)
    if result != "HELLO WORLD" {
        t.Errorf("Ожидалось 'HELLO WORLD', получено '%s'", result)
    }
}

Фикстуры для базы данных

Тестирование с реальной базой данных — сложная тема. Самый быстрый способ — использовать SQLite в памяти для изоляции.

package database

import (
    "database/sql"
    _ "github.com/mattn/go-s                

Блоги

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