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 Также в библиотеке
Работа с XML в Go: encoding/xml, библиотеки и примеры
httpmock для Go: полное руководство с примерами
JWT токены в PHP: Полное руководство по аутентификации и авторизации