gomock для Go: полное руководство по мокированию интерфейсов
1. Что это и зачем нужна
В современной разработке на Go тестирование играет ключевую роль. Однако часто код зависит от внешних систем: баз данных, HTTP-клиентов, файловых систем или других микросервисов. Тестировать такой код в изоляции сложно, так как эти зависимости требуют реального окружения. Здесь на помощь приходит gomock — мощная библиотека для генерации и использования моков (заглушек) для интерфейсов Go.
gomock позволяет автоматически создавать реализации интерфейсов, которые ведут себя предсказуемо: возвращают заданные значения, проверяют порядок вызовов, подсчитывают количество обращений и даже симулируют ошибки. Это делает юнит-тесты быстрыми, детерминированными и независимыми от внешних ресурсов. Библиотека тесно интегрируется со встроенным пакетом testing и является стандартом де-факто для мокирования в Go-сообществе.
Основная ценность gomock заключается в том, что вы тестируете поведение вашего кода, а не его реализацию. Вы проверяете, что ваш сервис правильно вызывает методы зависимостей с правильными аргументами и корректно обрабатывает результаты. Это особенно важно при следовании принципам SOLID и паттерну "Внедрение зависимостей" (Dependency Injection).
2. Установка
Для работы с gomock вам потребуется установить два компонента:
- Библиотека gomock — содержит рантайм для работы с моками.
- Утилита mockgen — генератор кода моков из интерфейсов.
Установка производится через стандартный менеджер пакетов Go:
# Установка библиотеки
go get github.com/golang/mock/gomock
# Установка утилиты mockgen
go install github.com/golang/mock/mockgen@v1.6.0
После установки убедитесь, что mockgen доступен в вашем PATH. Проверить можно командой:
mockgen --version
Если вы используете Go 1.16+, рекомендуется добавить mockgen в tools.go для версионирования:
// tools.go
// +build tools
package tools
import (
_ "github.com/golang/mock/mockgen"
)
3. Быстрый старт — минимальный рабочий пример
Рассмотрим простейший пример. Допустим, у нас есть интерфейс Greeter, который мы хотим замокировать.
// greeter.go
package main
// Greeter — интерфейс, который мы будем мокировать
type Greeter interface {
Greet(name string) string
}
// Service использует Greeter
type Service struct {
greeter Greeter
}
func NewService(g Greeter) *Service {
return &Service{greeter: g}
}
func (s *Service) SayHello(name string) string {
return s.greeter.Greet(name)
}
Сначала сгенерируем мок:
mockgen -source=greeter.go -destination=mock_greeter.go -package=main
Теперь напишем тест:
// greeter_test.go
package main
import (
"testing"
"github.com/golang/mock/gomock"
)
func TestSayHello(t *testing.T) {
// Создаем контроллер gomock
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// Создаем мок Greeter
mockGreeter := NewMockGreeter(ctrl)
// Настраиваем ожидание: при вызове Greet("Alice") вернуть "Hello, Alice!"
mockGreeter.EXPECT().Greet("Alice").Return("Hello, Alice!")
// Создаем сервис с моком
service := NewService(mockGreeter)
// Вызываем тестируемый метод
result := service.SayHello("Alice")
// Проверяем результат
if result != "Hello, Alice!" {
t.Errorf("Expected 'Hello, Alice!', got '%s'", result)
}
}
Запустите тест:
go test -v
Вы увидите, что тест проходит успешно. Мок автоматически проверил, что метод Greet был вызван ровно один раз с аргументом "Alice".
4. Основные методы и функции
Рассмотрим ключевые методы gomock, которые используются в 90% случаев. Каждый метод сопровождается сигнатурой и рабочим примером.
4.1. gomock.NewController(t)
Сигнатура: func NewController(t TestReporter) *Controller
Что делает: Создает контроллер gomock, который управляет жизненным циклом моков. Вызывает t.Fatal при нарушении ожиданий. Обязательно должен быть вызван defer ctrl.Finish().
ctrl := gomock.NewController(t)
defer ctrl.Finish()
4.2. mock.EXPECT()
Сигнатура: func (m *MockType) EXPECT() *MockTypeMockRecorder
Что делает: Возвращает объект-рекордер, который позволяет настроить ожидания для конкретного метода мока. Без вызова EXPECT() мок не будет проверять вызовы.
mockRepo.EXPECT().FindByID(1).Return(&User{ID: 1}, nil)
4.3. .Return(values ...interface{})
Сигнатура: func (c *Call) Return(rets ...interface{}) *Call
Что делает: Задает значения, которые будут возвращены при вызове метода. Количество и типы аргументов должны соответствовать сигнатуре метода.
mockCalculator.EXPECT().Add(2, 3).Return(5)
4.4. .Times(n int)
Сигнатура: func (c *Call) Times(n int) *Call
Что делает: Указывает, сколько раз должен быть вызван метод. По умолчанию — 1 раз. Можно задать 0 (не вызывать), MinTimes/MaxTimes для диапазона.
// Метод должен быть вызван ровно 3 раза
mockSender.EXPECT().Send(gomock.Any()).Return(nil).Times(3)
// Метод может быть вызван от 1 до 5 раз
mockSender.EXPECT().Send(gomock.Any()).Return(nil).MinTimes(1).MaxTimes(5)
4.5. gomock.Any()
Сигнатура: func Any() Matcher
Что делает: Сопоставляется с любым значением переданного типа. Используется, когда точное значение аргумента не важно для теста.
// Принимает любой ID и любой запрос
mockRepo.EXPECT().Update(gomock.Any(), gomock.Any()).Return(nil)
4.6. gomock.Eq(x interface{})
Сигнатура: func Eq(x interface{}) Matcher
Что делает: Сопоставляется только с точным значением x. Используется, когда нужно явно указать ожидаемое значение, отличное от литерала.
// Ожидаем вызов с точной строкой "admin"
mockAuth.EXPECT().CheckRole(gomock.Eq("admin")).Return(true)
4.7. gomock.Not(m Matcher)
Сигнатура: func Not(m Matcher) Matcher
Что делает: Инвертирует логику сопоставления. Возвращает true, если переданный matcher возвращает false.
// Ожидаем любой ID, кроме 0
mockRepo.EXPECT().FindByID(gomock.Not(gomock.Eq(0))).Return(&User{}, nil)
4.8. .Do(f interface{})
Сигнатура: func (c *Call) Do(f interface{}) *Call
Что делает: Выполняет функцию f при каждом вызове метода. Позволяет выполнить побочные действия (логирование, изменение состояния) или динамически вычислить возвращаемое значение.
// Динамический возврат: при каждом вызове возвращаем разное значение
var callCount int
mockIDGen.EXPECT().NextID().Do(func() {
callCount++
}).Return(42)
// Или более сложный пример с захватом аргументов
mockLogger.EXPECT().Log(gomock.Any()).Do(func(msg string) {
t.Logf("Лог сообщение: %s", msg)
})
4.9. .After(d time.Duration)
Сигнатура: func (c *Call) After(d time.Duration) *Call
Что делает: Указывает, что вызов должен произойти не ранее, чем через d времени после предыдущего вызова. Полезно для тестирования таймаутов и задержек.
// Первый вызов мгновенно, второй — через 100мс
mockAPI.EXPECT().Fetch().Return(data1, nil)
mockAPI.EXPECT().Fetch().Return(data2, nil).After(100 * time.Millisecond)
4.10. .InOrder(calls ...*Call)
Сигнатура: func InOrder(calls ...*Call)
Что делает: Глобальная функция, которая проверяет, что вызовы происходят в строго определенном порядке. Если порядок нарушен, тест падает.
// Строгий порядок: сначала Open, потом Write, потом Close
openCall := mockFile.EXPECT().Open("file.txt").Return(nil)
writeCall := mockFile.EXPECT().Write([]byte("data")).Return(4, nil)
closeCall := mockFile.EXPECT().Close().Return(nil)
gomock.InOrder(openCall, writeCall, closeCall)
5. Реальный практический пример
Рассмотрим полноценный пример: тестирование сервиса заказов, который зависит от репозитория пользователей, платежного шлюза и сервиса уведомлений.
// order_service.go
package main
import (
"errors"
"fmt"
)
// User представляет пользователя
type User struct {
ID int
Name string
Email string
}
// Order представляет заказ
type Order struct {
ID int
UserID int
Amount float64
Status string
}
// UserRepository — интерфейс для работы с пользователями
type UserRepository interface {
FindByID(id int) (*User, error)
UpdateBalance(userID int, amount float64) error
}
// PaymentGateway — интерфейс платежного шлюза
type PaymentGateway interface {
Charge(userID int, amount float64) (string, error) // возвращает ID транзакции
Refund(transactionID string) error
}
// NotificationService — интерфейс сервиса уведомлений
type NotificationService interface {
SendEmail(email, subject, body string) error
}
// OrderService — наш основной сервис
type OrderService struct {
users UserRepository
pay PaymentGateway
notify NotificationService
}
func NewOrderService(u UserRepository, p PaymentGateway, n NotificationService) *OrderService {
return &OrderService{users: u, pay: p, notify: n}
}
// PlaceOrder создает заказ и списывает средства
func (s *OrderService) PlaceOrder(userID int, amount float64) (*Order, error) {
// 1. Проверяем пользователя
user, err := s.users.FindByID(userID)
if err != nil {
return nil, fmt.Errorf("user not found: %w", err)
}
// 2. Списываем средства
txID, err := s.pay.Charge(userID, amount)
if err != nil {
return nil, fmt.Errorf("payment failed: %w", err)
}
// 3. Обновляем баланс пользователя
if err := s.users.UpdateBalance(userID, -amount); err != nil {
// В случае ошибки делаем возврат платежа
_ = s.pay.Refund(txID)
return nil, fmt.Errorf("balance update failed: %w", err)
}
// 4. Отправляем уведомление (не критично, если не удалось)
_ = s.notify.SendEmail(user.Email, "Order Confirmed",
fmt.Sprintf("Your order of $%.2f has been placed.", amount))
// 5. Возвращаем заказ
return &Order{
ID: 42,
UserID: userID,
Amount: amount,
Status: "confirmed",
}, nil
}
// CancelOrder отменяет заказ и возвращает средства
func (s *OrderService) CancelOrder(order *Order) error {
// Возвращаем платеж
if err := s.pay.Refund("tx_" + fmt.Sprint(order.ID)); err != nil {
return fmt.Errorf("refund failed: %w", err)
}
// Возвращаем баланс
if err := s.users.UpdateBalance(order.UserID, order.Amount); err != nil {
return fmt.Errorf("balance restore failed: %w", err)
}
// Уведомление
user, _ := s.users.FindByID(order.UserID)
if user != nil {
_ = s.notify.SendEmail(user.Email, "Order Cancelled",
fmt.Sprintf("Your order of $%.2f has been cancelled.", order.Amount))
}
return nil
}
// order_service_test.go
package main
import (
"errors"
"testing"
"github.com/golang/mock/gomock"
)
// Генерация моков (выполнить один раз):
// mockgen -source=order_service.go -destination=mocks_test.go -package=main
func TestPlaceOrder_Success(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// Создаем моки
mockUsers := NewMockUserRepository(ctrl)
mockPay := NewMockPaymentGateway(ctrl)
mockNotify := New