Avatar ·

Как правильно создавать моки (mock) для функций в Go при unit-тестировании?

Привет! Я начал изучать Go и пытаюсь писать unit-тесты для своего проекта. Столкнулся с проблемой: у меня есть функция, которая вызывает внешний сервис (например, отправляет email или делает HTTP-запрос). В тестах я не хочу реально вызывать эти сервисы, поэтому нужно создать мок (mock). Вот упрощённый пример моего кода: ```go package main import ( "fmt" "net/smtp" ) type EmailSender struct{} func (e *EmailSender) SendEmail(to, subject, body string) error { // Реальная отправка email через SMTP err := smtp.SendMail("smtp.example.com:587", nil, "from@example.com", []string{to}, []byte(body)) if err != nil { return fmt.Errorf("failed to send email: %w", err) } return nil } func NotifyUser(sender *EmailSender, userEmail string) error { return sender.SendEmail(userEmail, "Welcome", "Hello! Thanks for registering.") } ``` Я читал про интерфейсы и моки, но не понимаю, как правильно организовать код. Нужно ли создавать отдельный интерфейс для EmailSender? Как написать тест, который подменяет реальную отправку на заглушку? Я видел много примеров с testify/mock, но они кажутся слишком сложными для новичка. Подскажите, пожалуйста, лучший подход для мокирования в Go. Желательно с простым примером, чтобы я мог сразу применить в своём проекте. Заранее спасибо! <<>>

Правильный подход к мокированию в Go: интерфейсы и ручные моки

В Go принято использовать интерфейсы для абстракции зависимостей, а затем создавать ручные моки (manual mocks) или использовать библиотеки вроде testify/mock. Я покажу оба подхода, начиная с самого простого и идиоматичного для Go.

Шаг 1. Определяем интерфейс

Вместо того чтобы зависеть от конкретной структуры EmailSender, определите интерфейс с нужным методом:

package main

type EmailSender interface {
    SendEmail(to, subject, body string) error
}

// Реальная реализация
type RealEmailSender struct{}

func (r *RealEmailSender) SendEmail(to, subject, body string) error {
    // Здесь реальная логика отправки
    return nil
}

// Функция теперь принимает интерфейс
func NotifyUser(sender EmailSender, userEmail string) error {
    return sender.SendEmail(userEmail, "Welcome", "Hello!")
}

Шаг 2. Создаём ручной мок (самый простой способ)

Создайте структуру-заглушку, которая реализует тот же интерфейс:

// mock_email_sender.go (в том же пакете или в _test.go)
type MockEmailSender struct {
    Called      bool
    LastTo      string
    LastSubject string
    LastBody    string
    ShouldError bool
}

func (m *MockEmailSender) SendEmail(to, subject, body string) error {
    m.Called = true
    m.LastTo = to
    m.LastSubject = subject
    m.LastBody = body
    if m.ShouldError {
        return fmt.Errorf("simulated error")
    }
    return nil
}

Шаг 3. Пишем тест с моком

// main_test.go
package main

import (
    "testing"
)

func TestNotifyUser_Success(t *testing.T) {
    mockSender := &MockEmailSender{}
    err := NotifyUser(mockSender, "user@example.com")
    
    if err != nil {
        t.Errorf("unexpected error: %v", err)
    }
    
    if !mockSender.Called {
        t.Error("SendEmail was not called")
    }
    
    if mockSender.LastTo != "user@example.com" {
        t.Errorf("expected user@example.com, got %s", mockSender.LastTo)
    }
}

func TestNotifyUser_Error(t *testing.T) {
    mockSender := &MockEmailSender{ShouldError: true}
    err := NotifyUser(mockSender, "user@example.com")
    
    if err == nil {
        t.Error("expected error, got nil")
    }
}

Шаг 4. Альтернатива: testify/mock (для сложных случаев)

Если вам нужно больше гибкости (проверка количества вызовов, аргументов по порядку), используйте testify/mock:

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

// Mock с testify
type MockEmailSender struct {
    mock.Mock
}

func (m *MockEmailSender) SendEmail(to, subject, body string) error {
    args := m.Called(to, subject, body)
    return args.Error(0)
}

func TestNotifyUser_WithTestify(t *testing.T) {
    mockSender := new(MockEmailSender)
    mockSender.On("SendEmail", "user@example.com", "Welcome", "Hello!").Return(nil)
    
    err := NotifyUser(mockSender, "user@example.com")
    
    if err != nil {
        t.Errorf("unexpected error: %v", err)
    }
    mockSender.AssertExpectations(t)
}

Ключевые принципы мокирования в Go:

  • Программируйте на уровне интерфейсов — это основа гибкого тестирования в Go.
  • Начинайте с ручных моков — они просты, не требуют внешних зависимостей и полностью контролируемы.
  • Используйте testify/mock когда нужно проверять сложные сценарии: порядок вызовов, несколько раз с разными аргументами.
  • Не мокайте всё подряд — моки нужны только для внешних зависимостей (БД, API, файловая система). Внутреннюю логику тестируйте напрямую.

Такой подход делает ваш код тестируемым, а тесты — читаемыми и поддерживаемыми.

<<>>

Использование интерфейсов с замыканиями (functional options)

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

// Определяем тип функции
type SendEmailFunc func(to, subject, body string) error

func NotifyUser(sendEmail SendEmailFunc, userEmail string) error {
    return sendEmail(userEmail, "Welcome", "Hello!")
}

// В тесте просто передаём заглушку
func TestNotifyUser(t *testing.T) {
    var called bool
    mockSend := func(to, subject, body string) error {
        called = true
        if to != "user@example.com" {
            t.Errorf("unexpected to: %s", to)
        }
        return nil
    }
    
    err := NotifyUser(mockSend, "user@example.com")
    if err != nil {
        t.Errorf("unexpected error: %v", err)
    }
    if !called {
        t.Error("function was not called")
    }
}

Плюсы этого подхода:

  • Минимум кода — не нужно создавать отдельные структуры для моков
  • Отлично подходит для простых случаев (один метод, один вызов)
  • Не требует сторонних библиотек

Минусы:

  • Если у интерфейса много методов, такой подход становится громоздким
  • Сложнее проверять порядок вызовов

Выбирайте этот вариант, когда ваш сервис имеет всего 1-2 метода и вы не планируете расширять его в будущем.