gomock для Go: полное руководство с примерами

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

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

Начать курс

gomock для Go: полное руководство по мокированию интерфейсов

1. Что это и зачем нужна

В современной разработке на Go тестирование играет ключевую роль. Однако часто код зависит от внешних систем: баз данных, HTTP-клиентов, файловых систем или других микросервисов. Тестировать такой код в изоляции сложно, так как эти зависимости требуют реального окружения. Здесь на помощь приходит gomock — мощная библиотека для генерации и использования моков (заглушек) для интерфейсов Go.

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

Основная ценность gomock заключается в том, что вы тестируете поведение вашего кода, а не его реализацию. Вы проверяете, что ваш сервис правильно вызывает методы зависимостей с правильными аргументами и корректно обрабатывает результаты. Это особенно важно при следовании принципам SOLID и паттерну "Внедрение зависимостей" (Dependency Injection).

2. Установка

Для работы с gomock вам потребуется установить два компонента:

  1. Библиотека gomock — содержит рантайм для работы с моками.
  2. Утилита 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                    

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