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

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

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

Начать курс

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

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

Mockery — это мощный инструмент для генерации мок-объектов (mock objects) на языке Go. В современной разработке на Go, где интерфейсы играют ключевую роль, тестирование изолированных компонентов часто требует создания заглушек для внешних зависимостей. Mockery автоматизирует этот процесс, генерируя полноценные реализации интерфейсов, которые можно использовать в unit-тестах. Вместо того чтобы вручную писать десятки строк кода для каждого интерфейса, вы просто запускаете одну команду и получаете готовый мок со всеми необходимыми методами.

Основная задача mockery — упростить тестирование кода, который зависит от внешних сервисов (базы данных, API, файловые системы и т.д.). Сгенерированные моки позволяют контролировать возвращаемые значения, проверять количество вызовов методов и аргументы, с которыми они были вызваны. Это особенно полезно при разработке микросервисов, где каждый компонент должен тестироваться изолированно. Библиотека использует популярный пакет testify/mock в качестве основы, что обеспечивает совместимость с большинством Go-проектов.

Mockery поддерживает как простые интерфейсы с несколькими методами, так и сложные иерархии. Она умеет генерировать моки для встроенных типов, интерфейсов из сторонних пакетов и даже для generic-интерфейсов (начиная с Go 1.18). Благодаря гибкой системе конфигурации, вы можете контролировать имена файлов, пакеты, в которые помещаются моки, и даже добавлять собственные методы. Это делает mockery незаменимым инструментом в арсенале любого Go-разработчика, стремящегося к качественному и поддерживаемому коду.

2. Установка

Установка mockery выполняется через стандартный менеджер пакетов Go. Для этого выполните команду:

go install github.com/vektra/mockery/v2@latest

После установки убедитесь, что бинарный файл находится в вашем $PATH. Обычно он устанавливается в $GOPATH/bin или $HOME/go/bin. Вы можете проверить установку командой:

mockery --version

Если вы используете Go модули, рекомендуется добавить mockery в качестве инструмента разработки. Для этого создайте или отредактируйте файл tools.go в корне проекта:

//go:build tools
// +build tools

package tools

import (
    _ "github.com/vektra/mockery/v2"
)

Затем выполните:

go mod tidy

Это гарантирует, что версия mockery будет зафиксирована в вашем go.mod файле.

3. Быстрый старт — минимальный рабочий пример

Рассмотрим простой пример. Допустим, у нас есть интерфейс UserRepository:

// user.go
package main

type User struct {
    ID   int
    Name string
}

type UserRepository interface {
    GetUser(id int) (*User, error)
    SaveUser(user *User) error
}

Для генерации мока выполните команду в корне проекта:

mockery --name=UserRepository --output=mocks --with-expecter

Эта команда создаст файл mocks/UserRepository.go с реализацией мока. Теперь мы можем использовать его в тесте:

// user_test.go
package main

import (
    "testing"
    "github.com/stretchr/testify/assert"
    "your-project/mocks"
)

func TestGetUser(t *testing.T) {
    // Создаем мок
    mockRepo := mocks.NewUserRepository(t)
    
    // Настраиваем ожидаемое поведение
    expectedUser := &User{ID: 1, Name: "Alice"}
    mockRepo.On("GetUser", 1).Return(expectedUser, nil)
    
    // Вызываем тестируемую функцию
    user, err := mockRepo.GetUser(1)
    
    // Проверяем результаты
    assert.NoError(t, err)
    assert.Equal(t, expectedUser, user)
    
    // Проверяем, что все ожидаемые вызовы были выполнены
    mockRepo.AssertExpectations(t)
}

Этот минимальный пример показывает, как легко и быстро можно начать использовать mockery для тестирования.

4. Основные методы/функции

4.1. mock.On(methodName string, arguments ...interface{}) *mock.Call

Устанавливает ожидание вызова метода с определенными аргументами. Возвращает объект *mock.Call, который можно дополнительно настроить.

mockRepo.On("GetUser", 1).Return(&User{ID: 1}, nil)
mockRepo.On("SaveUser", mock.Anything).Return(nil)

4.2. mock.Return(returnArguments ...interface{}) *mock.Call

Задает возвращаемые значения для ожидаемого вызова. Аргументы должны соответствовать сигнатуре метода.

mockRepo.On("GetUser", 1).Return(&User{ID: 1, Name: "Bob"}, nil)
mockService.On("ProcessOrder", mock.Anything).Return("success", 200, nil)

4.3. mock.AssertExpectations(t mock.TestingT) bool

Проверяет, что все ожидаемые вызовы были выполнены. Если какой-то вызов не был сделан, тест упадет с сообщением об ошибке.

func TestOrderProcessing(t *testing.T) {
    mockService := mocks.NewOrderService(t)
    mockService.On("ValidateOrder", mock.Anything).Return(true)
    mockService.On("ChargePayment", mock.Anything).Return(nil)
    
    // ... тестовый код ...
    
    mockService.AssertExpectations(t)
}

4.4. mock.AssertCalled(t mock.TestingT, methodName string, arguments ...interface{}) bool

Проверяет, что метод был вызван хотя бы один раз с указанными аргументами. В отличие от AssertExpectations, проверяет только конкретный метод.

mockRepo.On("GetUser", 1).Return(&User{ID: 1}, nil)
// ... выполнение кода ...
mockRepo.AssertCalled(t, "GetUser", 1)
mockRepo.AssertCalled(t, "GetUser", mock.AnythingOfType("int"))

4.5. mock.AssertNotCalled(t mock.TestingT, methodName string, arguments ...interface{}) bool

Проверяет, что метод НЕ был вызван с указанными аргументами. Полезно для проверки, что определенные пути выполнения кода не были задействованы.

mockRepo.On("GetUser", 1).Return(nil, errors.New("not found"))
// ... выполнение кода с обработкой ошибки ...
mockRepo.AssertNotCalled(t, "SaveUser", mock.Anything)

4.6. mock.On(methodName string, arguments ...interface{}).Once()

Указывает, что ожидаемый вызов должен произойти ровно один раз. Это частный случай .Times(1).

mockRepo.On("GetUser", 1).Return(&User{ID: 1}, nil).Once()
mockRepo.On("SaveUser", mock.Anything).Return(nil).Once()

4.7. mock.On(methodName string, arguments ...interface{}).Times(n int)

Указывает точное количество ожидаемых вызовов метода. Если метод будет вызван другое количество раз, тест упадет.

mockRepo.On("GetUser", 1).Return(&User{ID: 1}, nil).Times(2)
mockRepo.On("ProcessItems", mock.Anything).Return(nil).Times(3)

4.8. mock.On(methodName string, arguments ...interface{}).Run(func(args mock.Arguments))

Позволяет выполнить произвольную функцию при вызове метода. Это полезно для проверки аргументов или выполнения побочных эффектов.

mockRepo.On("SaveUser", mock.Anything).Return(nil).Run(func(args mock.Arguments) {
    user := args.Get(0).(*User)
    assert.Equal(t, "Alice", user.Name)
    // Можно также изменять аргументы
    user.ID = 42
})

4.9. mock.On(methodName string, arguments ...interface{}).Return(returns ...interface{}).Maybe()

Указывает, что вызов метода является опциональным. Если метод не будет вызван, тест не упадет. Полезно для тестирования условной логики.

mockRepo.On("GetUser", 1).Return(&User{ID: 1}, nil).Maybe()
mockRepo.On("GetUser", 2).Return(nil, errors.New("not found")).Maybe()

4.10. mock.On(methodName string, arguments ...interface{}).Return(returns ...interface{}).NotBefore(call *mock.Call)

Указывает, что данный вызов должен произойти после указанного вызова. Позволяет контролировать порядок вызовов методов.

createCall := mockRepo.On("CreateUser", mock.Anything).Return(&User{ID: 1}, nil)
mockRepo.On("SendWelcomeEmail", 1).Return(nil).NotBefore(createCall)

5. Реальный практический пример

Рассмотрим полноценный пример тестирования сервиса управления заказами в интернет-магазине. У нас будет три компонента: репозиторий заказов, платежный шлюз и сервис уведомлений.

// models.go
package models

type Order struct {
    ID     int
    UserID int
    Amount float64
    Status string
}

type PaymentResult struct {
    TransactionID string
    Success       bool
    Error         error
}

// interfaces.go
package interfaces

type OrderRepository interface {
    GetOrder(id int) (*models.Order, error)
    SaveOrder(order *models.Order) error
    UpdateOrderStatus(id int, status string) error
}

type PaymentGateway interface {
    Charge(amount float64, userID int) (*models.PaymentResult, error)
    Refund(transactionID string) error
}

type NotificationService interface {
    SendOrderConfirmation(order *models.Order) error
    SendPaymentFailedNotification(userID int, reason string) error
}

// order_service.go
package service

type OrderService struct {
    repo     interfaces.OrderRepository
    payment  interfaces.PaymentGateway
    notifier interfaces.NotificationService
}

func NewOrderService(
    repo interfaces.OrderRepository,
    payment interfaces.PaymentGateway,
    notifier interfaces.NotificationService,
) *OrderService {
    return &OrderService{
        repo:     repo,
        payment:  payment,
        notifier: notifier,
    }
}

func (s *OrderService) ProcessOrder(orderID int) error {
    // Получаем заказ
    order, err := s.repo.GetOrder(orderID)
    if err != nil {
        return err
    }
    
    // Проверяем, что заказ еще не обработан
    if order.Status == "processed" {
        return nil
    }
    
    // Выполняем платеж
    result, err := s.payment.Charge(order.Amount, order.UserID)
    if err != nil {
        s.notifier.SendPaymentFailedNotification(order.UserID, err.Error())
        return err
    }
    
    if !result.Success {
        s.notifier.SendPaymentFailedNotification(order.UserID, "Payment declined")
        return errors.New("payment declined")
    }
    
    // Обновляем статус заказа
    order.Status = "processed"
    if err := s.repo.UpdateOrderStatus(order.ID, "processed"); err != nil {
        // Откатываем платеж
        s.payment.Refund(result.TransactionID)
        return err
    }
    
    // Отправляем подтверждение
    s.notifier.SendOrderConfirmation(order)
    
    return nil
}

// order_service_test.go
package service_test

import (
    "errors"
    "testing"
    
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
    
    "your-project/mocks"
    "your-project/models"
)

func TestProcessOrder_Success(t *testing.T) {
    // Создаем моки
    mockRepo := mocks.NewOrderRepository(t)
    mockPayment := mocks.NewPaymentGateway(t)
    mockNotifier := mocks.NewNotificationService(t)
    
    // Создаем сервис
    service := NewOrderService(mockRepo, mockPayment, mockNotifier)
    
    // Настраиваем ожидания
    order := &models.Order{
        ID:     1,
        UserID: 100,
        Amount: 99.99,
        Status: "pending",
    }
    
    mockRepo.On("GetOrder", 1).Return(order, nil).Once()
    mockPayment.On("Charge", 99.99, 100).Return(&models.PaymentResult{
        TransactionID: "txn_123",
        Success:       true,
    }, nil).Once()
    mockRepo.On("UpdateOrderStatus", 1, "processed").Return(nil).Once()
    mockNotifier.On("SendOrderConfirmation", order).Return(nil).Once()
    
    // Выполняем тестируемый метод
    err := service.ProcessOrder(1)
    
    // Проверяем результаты
    assert.NoError(t, err)
    mockRepo.AssertExpectations(t)
    mockPayment.AssertExpectations(t)
    mockNotifier.AssertExpectations(t)
}

func TestProcessOrder_PaymentFailed(t *testing.T) {
    mockRepo := mocks.NewOrderRepository(t)
    mockPayment := mocks.NewPaymentGateway(t)
    mockNotifier := mocks.NewNotificationService(t)
    
    service := NewOrderService(mockRepo, mockPayment, mockNotifier)
    
    order := &models.Order{
        ID:     1,
        UserID: 100,
        Amount: 99.99,
        Status: "pending",
    }
    
    mockRepo.On("GetOrder", 1).Return(order, nil).Once()
    mockPayment.On("Charge", 99.99, 100).Return(nil, errors.New("insufficient funds")).Once()
    mockNotifier.On("SendPaymentFailedNotification", 100, "insufficient funds").Return(nil).Once()
    
    err := service.ProcessOrder(1)
    
    assert.Error(t, err)
    assert.Contains(t, err.Error(), "insufficient funds")
    mockRepo.AssertExpectations(t)
    mockPayment.AssertExpectations(t)
    mockNotifier.AssertExpectations(t)
}

func TestProcessOrder_UpdateFailsWithRollback(t *testing.T) {
    mockRepo := mocks.NewOrderRepository(t)
    mockPayment := mocks.NewPaymentGateway(t)
    mockNotifier := mocks.NewNotificationService(t)
    
    service := NewOrderService(mockRepo, mockPayment, mockNotifier)
    
    order := &models.Order{
        ID                    

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