Что такое моки (mocks)?go-68

Моки (mocks) — это специальные объекты, которые имитируют поведение реальных зависимостей в тестах, позволяя изолировать тестируемый код и контролировать его окружение.

1. Основные концепции моков

  • Имитация поведения: Моки заменяют реальные зависимости (БД, API, сервисы)
  • Контроль окружения: Позволяют задавать ожидаемые входы и выходы
  • Верификация вызовов: Проверяют, как тестируемый код взаимодействует с зависимостями
  • Изоляция тестов: Позволяют тестировать код без реальных внешних систем

2. Пример мока в Go

Рассмотрим пример с моком хранилища:

// Реальный интерфейс хранилища
type UserRepository interface {
    GetUser(id int) (*User, error)
    SaveUser(user *User) error
}

// Реализация мока
type MockUserRepository struct {
    GetUserFunc    func(id int) (*User, error)
    SaveUserFunc   func(user *User) error
    GetUserCalled  bool
    SaveUserCalled bool
}

func (m *MockUserRepository) GetUser(id int) (*User, error) {
    m.GetUserCalled = true
    return m.GetUserFunc(id)
}

func (m *MockUserRepository) SaveUser(user *User) error {
    m.SaveUserCalled = true
    return m.SaveUserFunc(user)
}

// Использование в тесте
func TestUserService(t *testing.T) {
    mockRepo := &MockUserRepository{
        GetUserFunc: func(id int) (*User, error) {
            return &User{ID: id, Name: "Test User"}, nil
        },
    }

    service := NewUserService(mockRepo)
    user, err := service.GetUser(123)

    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if user.Name != "Test User" {
        t.Errorf("unexpected user name: %s", user.Name)
    }
    if !mockRepo.GetUserCalled {
        t.Error("GetUser was not called")
    }
}

3. Популярные библиотеки для моков в Go

3.1. Gomock

// Генерация мока: mockgen -source=repository.go -destination=repository_mock.go
ctrl := gomock.NewController(t)
defer ctrl.Finish()

mockRepo := NewMockUserRepository(ctrl)
mockRepo.EXPECT().
    GetUser(gomock.Eq(123)).
    Return(&User{Name: "John"}, nil)

service := NewUserService(mockRepo)
// ... вызов тестируемого кода

3.2. Testify Mock

mockRepo := new(MockUserRepository)
mockRepo.On("GetUser", 123).Return(&User{Name: "John"}, nil)

service := NewUserService(mockRepo)
// ... вызов тестируемого кода

mockRepo.AssertExpectations(t)  // Проверка всех ожиданий

4. Когда использовать моки

  1. Тестирование бизнес-логики без реальной БД
  2. Имитация ошибок (например, ошибок сети)
  3. Проверка взаимодействия между компонентами
  4. Тестирование сценариев, которые сложно воспроизвести с реальными системами

5. Альтернативы мокам

  • Стабы (Stubs): Простые реализации, возвращающие фиксированные значения
  • Фейки (Fakes): Упрощенные рабочие реализации (например, in-memory БД)
  • Спаи (Spies): Обертки, которые записывают вызовы для последующей проверки

Пример фейка:

type FakeUserRepository struct {
    users map[int]*User
}

func (f *FakeUserRepository) GetUser(id int) (*User, error) {
    return f.users[id], nil
}

6. Best Practices

  1. Не переусердствуйте: Моки нужны только для внешних зависимостей
  2. Документируйте ожидания: Четко описывайте, что должен делать мок
  3. Поддерживайте актуальность: Моки должны соответствовать реальным интерфейсам
  4. Избегайте хрупких тестов: Не проверяйте лишние детали реализации
  5. Комбинируйте подходы: Используйте моки вместе с другими тестовыми двойниками

Резюмируем

Моки в Go — это мощный инструмент для:

  • Изоляции тестируемого кода от внешних зависимостей
  • Контроля поведения зависимостей в тестах
  • Проверки взаимодействия между компонентами
  • Тестирования сложных сценариев и ошибок

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