Что такое моки (mocks) и для чего они используются в тестировании?

Ответ

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

Моки используются в юнит- и интеграционном тестировании для замены сложных, медленных или недоступных зависимостей, таких как базы данных, внешние API, файловая система или другие сервисы.

Основные цели использования моков:

  1. Изоляция: Позволяют тестировать один компонент (например, сервис), не затрагивая его зависимости. Это гарантирует, что тест провалится только из-за ошибки в тестируемом коде, а не во внешней системе.
  2. Эмуляция сценариев: Дают возможность имитировать любое поведение зависимости: успешный ответ, различные виды ошибок (сетевая, "не найдено"), задержки ответа.
  3. Ускорение тестов: Замена реальных сетевых вызовов или обращений к диску на быстрые вызовы в памяти значительно сокращает время выполнения тестов.
  4. Проверка взаимодействий: Моки позволяют проверить, что тестируемый код правильно взаимодействует со своей зависимостью — например, вызывает нужный метод с правильными аргументами.

Пример мока в Go с использованием testify/mock:

// Интерфейс, который мы будем мокировать
type DBLayer interface {
    GetUser(id int) (*User, error)
}

// Структура мока
type MockDB struct {
    mock.Mock
}

// Реализация метода интерфейса для мока
func (m *MockDB) GetUser(id int) (*User, error) {
    // .Called() регистрирует вызов и возвращает заранее заданные значения
    args := m.Called(id)
    // Безопасно извлекаем результаты
    if args.Get(0) == nil {
        return nil, args.Error(1)
    }
    return args.Get(0).(*User), args.Error(1)
}

// Тест, использующий мок
func TestUserService(t *testing.T) {
    // 1. Создаем экземпляр мока
    mockDB := new(MockDB)

    // 2. Настраиваем ожидаемое поведение: при вызове GetUser(1) вернуть юзера и nil-ошибку
    mockDB.On("GetUser", 1).Return(&User{Name: "John"}, nil)

    // 3. Внедряем мок в тестируемый сервис
    service := NewUserService(mockDB)
    user, err := service.FetchUser(1)

    // 4. Проверяем результат
    assert.NoError(t, err)
    assert.Equal(t, "John", user.Name)

    // 5. Убеждаемся, что все настроенные ожидания были выполнены
    mockDB.AssertExpectations(t)
}

В итоге, моки — это мощный инструмент для написания надежных, быстрых и предсказуемых тестов.

Ответ 18+ 🔞

Да ты посмотри, что творят эти ваши моки, блядь! Это ж просто пиздец какой-то, а не технология. Ну, слушай сюда, сейчас разложу по полочкам, как эти штуки работают, чтобы ты не тупил, как Герасим из той истории, который только "Му-му" и мог сказать.

Моки (mock-объекты) — это, грубо говоря, такие подставные ублюдки, которые притворяются настоящими зависимостями твоего кода. Они — часть большой и дружной банды тестовых двойников (Test Doubles), куда ещё входят всякие стабы и фейки, но это уже другая песня.

Зачем они нужны, эти мартышлюшки? А вот зачем, блядь:

  1. Изоляция, ёпта! Чтобы твой тест проверял именно твой код, а не падал потому, что база данных легла, как Муму в том мешке. Ты же не хочешь, чтобы твои тесты были зависимы от какой-то левой внешней хуйни?
  2. Сценарии на любой вкус. Хочешь, чтобы твой "внешний сервис" вернул успех? Пожалуйста. Хочешь, чтобы он нагло послал тебя с ошибкой 500? Легко! Хочешь, чтобы он просто завис на полчаса? Да хуй с ним, можно и это сэмулировать. Полный карт-бланш, блядь!
  3. Скорость, ядрёна вошь! Зачем ждать, пока реальная база выполнит запрос, если можно просто подменить её куском памяти, который отработает за микросекунды? Тесты должны бегать быстро, а не ползать, как черепаха в сиропе.
  4. Контроль за общением. Мок — он как стукач. Он запоминает, кто к нему обращался, с какими аргументами и сколько раз. Потом ты можешь его допросить: "А вызывал ли меня метод SaveUser с вот этим ебаным email'ом?". И он тебе честно ответит. Или соврёт, если ты его неправильно настроил, пидарас шерстяной.

Смотри, как это выглядит в Go, на примере библиотеки testify/mock:

// Вот есть у нас интерфейс, который мы хотим подменить. Допустим, это работа с базой.
type DBLayer interface {
    GetUser(id int) (*User, error)
}

// А вот и наш подставной мудак — мок. Он реализует тот же интерфейс.
type MockDB struct {
    mock.Mock // Вот эта штука даёт ему все силы для обмана.
}

// Он притворяется, что умеет получать пользователя.
func (m *MockDB) GetUser(id int) (*User, error) {
    // .Called() — это как крик "Ата-та! Меня вызвали!". Запоминает вызов.
    args := m.Called(id)
    // А дальше он просто возвращает то, что мы ему заранее велели.
    if args.Get(0) == nil {
        return nil, args.Error(1)
    }
    return args.Get(0).(*User), args.Error(1)
}

// А вот сам тест, где мы разводим эту цирковую постановку.
func TestUserService(t *testing.T) {
    // 1. Вызываем нашего двойника на сцену.
    mockDB := new(MockDB)

    // 2. Пишем ему сценарий: "Чувак, когда у тебя вызовут GetUser с цифрой 1, ты верни вот этого Джона и скажи, что всё ок".
    mockDB.On("GetUser", 1).Return(&User{Name: "John"}, nil)

    // 3. Подсовываем этого актёра нашему сервису вместо настоящей базы.
    service := NewUserService(mockDB)
    user, err := service.FetchUser(1) // Сервис думает, что работает с реальностью, ага, щас!

    // 4. Проверяем, что наш сервис не обосрался и правильно обработал "ответ базы".
    assert.NoError(t, err)
    assert.Equal(t, "John", user.Name)

    // 5. И самое главное — допрашиваем мока: "А вызывали ли тебя так, как мы договаривались?"
    mockDB.AssertExpectations(t) // Если нет — тест провален, потому что логика пошла не по плану.
}

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