Расскажите о вашем опыте мокирования зависимостей в Go. Какие подходы и инструменты вы использовали?

Ответ

Мокирование в Go — это ключевая техника для написания изолированных юнит-тестов. Его основная цель — заменить реальные зависимости (например, базу данных или внешний API) на контролируемые тестовые двойники (моки).

В Go это достигается преимущественно через интерфейсы.

Подходы к мокированию:

  1. Ручное мокирование Простой способ, при котором вы вручную создаете структуру, реализующую нужный интерфейс. Подходит для простых интерфейсов.

    // Интерфейс нашей зависимости
    type UserStorage interface {
        GetUser(id int) (string, error)
    }
    
    // Ручной мок
    type MockUserStorage struct {
        User  string
        Error error
    }
    
    func (m *MockUserStorage) GetUser(id int) (string, error) {
        return m.User, m.Error
    }
    
    // Тест, использующий мок
    func TestUserService(t *testing.T) {
        mockDB := &MockUserStorage{User: "test_user", Error: nil}
        service := NewUserService(mockDB)
        // ... дальнейшее тестирование логики сервиса
    }
  2. Автоматическая генерация моков Для сложных интерфейсов ручное создание моков становится утомительным. Здесь помогают инструменты кодогенерации.

Популярные инструменты:

  • gomock (от Google): Официальный и очень мощный инструмент. Он генерирует моки из интерфейсов с помощью утилиты mockgen. Его особенность — строгий контроль вызовов через controller.

    // Пример теста с gomock
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()
    
    mockDB := NewMockUserStorage(ctrl) // Сгенерированный мок
    mockDB.EXPECT().GetUser(1).Return("test_user", nil).Times(1)
    
    service := NewUserService(mockDB)
    // ...
  • testify/mock: Часть популярной библиотеки testify. Более гибкий и менее строгий, чем gomock. Не требует обязательной кодогенерации (хотя отлично работает в паре с mockery), что делает его проще для старта.

  • mockery: Утилита для генерации моков, которая отлично интегрируется с testify/mock.

Ключевой вывод: Независимо от инструмента, основа хорошего мокирования в Go — это проектирование кода с использованием интерфейсов для отделения бизнес-логики от ее зависимостей.

Ответ 18+ 🔞

А, мокирование в Go, говоришь? Ну это ж святое дело, блядь! Это когда ты свою логику от всех этих баз данных и внешних апишек отвязываешь, чтобы тесты не превращались в ебаный цирк с конями, где всё падает из-за того, что какой-то сервис лег.

Вот смотри, основа всего этого шапито — интерфейсы. Без них нихуя не получится. Это как если бы ты пришел на стройку без лопаты — можешь ковырять землю руками, но заебёшься, блядь.

Как это, сука, делают?

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

    // Вот есть у нас какая-то хрень, которая пользователей ищет
    type UserStorage interface {
        GetUser(id int) (string, error)
    }
    
    // А вот наш рукожопый мок для неё
    type MockUserStorage struct {
        User  string
        Error error
    }
    
    func (m *MockUserStorage) GetUser(id int) (string, error) {
        return m.User, m.Error // Просто отдаём то, что заранее всунули. Никаких сюрпризов, епта.
    }
    
    // И тест
    func TestUserService(t *testing.T) {
        mockDB := &MockUserStorage{User: "test_user", Error: nil}
        service := NewUserService(mockDB)
        // ... и дальше проверяем, что наш сервис не обосрался с такими данными
    }

    Всё просто, как три копейки. Но если интерфейс на два листа — писать такой мок замучаешься, волнение ебать.

  2. Автоматом, как цивилизованные люди Вот для этого и придумали инструменты, чтобы не ебать себе мозг.

Инструменты на любой вкус и цвет

  • gomock от Гугла — это такой строгий дядька в очках. Официальный, мощный, но любит, чтобы всё было по правилам. Требует кодогенерации через mockgen. Зато он следит, чтобы ты вызвал метод ровно столько раз, сколько сказал. Раз сказал — Times(1), а вызвал дважды? Получай ошибку, пидарас шерстяной!

    ctrl := gomock.NewController(t)
    defer ctrl.Finish() // Важно не забыть, а то он тебе в тестах мозг выест
    
    mockDB := NewMockUserStorage(ctrl) // Мок, сгенерированный автоматом
    // Вот тут магия: говорим, чего ждём
    mockDB.EXPECT().GetUser(1).Return("test_user", nil).Times(1)
    
    service := NewUserService(mockDB)
    // ...
  • testify/mock — более расслабленный парень. Часть большой библиотеки testify. Его можно и руками написать, но обычно его генерируют через...

  • mockery — вот этот чувак как раз и наклепает тебе моков для testify/mock по щелчку пальцев. Удобно, гибко, не так строго, как гугловский.

А главная мысль, которую ты должен вынести, как кот сука собака: чтобы всё это великолепие работало, нужно изначально писать код так, чтобы он зависел от интерфейсов, а не от конкретных структур. Тогда подсунуть мок вместо настоящей базы — раз плюнуть. Иначе будешь потом рефакторить всё, блядь, с горящими глазами и матом в голосе.