Как правильно создавать моки (mock) для функций в Go при unit-тестировании?
Правильный подход к мокированию в 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 метода и вы не планируете расширять его в будущем.