5. testify mock

Если фреймворк gomock (подробно рассмотренный в главе 4. gomock. Генерация моков) представляет собой строгую, академическую парадигму с принудительной кодогенерацией и жесткой типизацией, то пакет testify/mock олицетворяет прагматичный и лаконичный подход. Он является органичной частью вселенной testify, благодаря чему естественно сочетается с привычными инструментами утверждений assert и require.

Фундаментальное преимущество testify/mock заключается в его гибкости: он не обязывает разработчика выстраивать сложный конвейер кодогенерации (хотя при желании она легко подключается через утилиту mockery) и предоставляет интуитивно понятный, человекочитаемый API для регистрации ожиданий.


Анатомия теста с testify/mock

Архитектурным ядром библиотеки является встроенная структура mock.Mock. Вы внедряете ее посредством композиции в свой тестовый дублер. Вместо генерации сотен строк машинного кода вы просто связываете вызов метода с центральным диспетчером через строковые идентификаторы:

Go
package store_test

import (
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
    "github.com/stretchr/testify/require"
)

// 1. Описываем структуру мока вручную (или генерируем через mockery)
type RepositoryMock struct {
    mock.Mock
}

func (m *RepositoryMock) Get(id int) (string, error) {
    // Метод Called регистрирует факт обращения, упаковывает аргумент id
    // и отыскивает подходящее ожидание в таблице вызовов
    args := m.Called(id)

    // Извлекаем возвращаемые значения через вспомогательные кастеры типов
    return args.String(0), args.Error(1)
}

func TestService(t *testing.T) {
    repo := new(RepositoryMock)

    // 2. Декларативная регистрация ожиданий (On)
    // Ожидаем вызов Get с аргументом 42 ровно один раз
    repo.On("Get", 42).Return("success", nil).Once()

    // 3. Вызов тестируемого бизнес-компонента
    result, err := MyService(repo, 42)

    // 4. Проверка результатов доменного слоя
    require.NoError(t, err)
    assert.Equal(t, "success", result)

    // 5. Верификация: все ли ожидания, зарегистрированные в On, были удовлетворены
    repo.AssertExpectations(t)
}
flowchart TD
    subgraph Setup["Настройка ожиданий"]
        RepoInit["repo := new(RepositoryMock)"]
        OnCall["repo.On('Get', 42).Return('success', nil).Once()"]
        RepoInit --> OnCall
    end

    subgraph Execution["Выполнение метода"]
        ServiceCall["MyService(repo, 42)"] --> Method["m.Called(id)"]
        Method --> Matcher{"Поиск в ExpectedCalls<br/>по имени и reflect.DeepEqual"}
        Matcher -->|"Найдено"| Found["Возврат Arguments:<br/>args.String(0), args.Error(1)"]
        Matcher -->|"Не найдено"| Panic["Фатальный panic:<br/>Unexpected method call"]
    end

    subgraph Teardown["Верификация контракта"]
        Assert["repo.AssertExpectations(t)"]
    end

    OnCall --> ServiceCall
    Found --> Assert

    style Setup fill:#0f172a,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    style Execution fill:#1e1b4b,stroke:#a855f7,stroke-width:2px,color:#f8fafc
    style Panic fill:#1e1b4b,stroke:#ef4444,stroke-width:2px,color:#fca5a5
    style Teardown fill:#064e3b,stroke:#10b981,stroke-width:2px,color:#6ee7b7
  

Гибкость матчеров и аргументов

Библиотека testify/mock позволяет игнорировать малозначительные для конкретного теста параметры или валидировать сложные вложенные структуры с помощью функциональных предикатов:

  • mock.Anything: всеядный матчер, выступающий полным семантическим аналогом gomock.Any().
  • mock.MatchedBy(func): мощнейший инструмент точечной фильтрации, позволяющий передать предикат и проинспектировать конкретное поле переданного объекта.
Go
// Проверяем, что в репозиторий уходит пользователь с непустым Email
repo.On("Save", mock.MatchedBy(func(u *User) bool {
    return u != nil && u.Email != ""
})).Return(nil)

Автоматизация через Mockery

Если ваш проект насчитывает десятки многометодных интерфейсов, выписывать m.Called(id) и вспомогательные функции руками становится обременительно. Для автоматизации генерации дублеров в стиле testify сообщество создало превосходную утилиту — mockery.

Преимущества mockery перед mockgen:

  1. Генерируемый код лаконичен и легко читается человеком.
  2. Поддерживается пакетная генерация одной командой без необходимости расставлять директивы в каждом файле:

    Bash
    mockery --all
    

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


Mechanical Sympathy: Цена рефлексии в testify

В отличие от gomock, где сгенерированный код компилируется в жестко типизированные машинные инструкции, testify/mock функционирует за счет динамической рефлексии и пустых интерфейсов.


Сравнение с gomock

Архитектурный критерий gomock (mockgen) testify/mock (mockery)
Контроль типов Строгий (ошибки ловятся компилятором) Мягкий (проверка строк и типов в рантайме)
Кодогенерация Обязательна (mockgen) Опциональна (легко пишется вручную или через mockery)
Читаемость кода Многословная, громоздкая цепочка EXPECT() Лаконичный, декларативный стиль On().Return()
Интеграция Автономный фреймворк Google/Uber Нативная часть монолитной экосистемы testify

Итог

  1. testify/mock — великолепный инструмент для проектов, где стандартный стек тестирования уже опирается на testify/assert и testify/require.
  2. Финальный вызов repo.AssertExpectations(t) обязателен: именно он проверяет, что все запланированные сценарии взаимодействия действительно состоялись.
  3. Для снижения рутины комбинируйте библиотеку с генератором mockery, чтобы не писать вызовы Called() и Return() руками.
  4. Помните об архитектурной цене рефлексии: параметры упаковываются в interface{}, что провоцирует работу сборщика мусора и переносит ошибки опечаток в строках из компилятора в рантайм.

В мире тестирования легко утонуть в многообразии понятий. Используя testify/mock, мы создаем полноценный интеллектуальный Mock. Но нередко для чистоты сценария нам требуется более простая или, напротив, более функциональная конструкция.

Чтобы разложить по полочкам классические категории дублеров и навсегда исключить путаницу на собеседованиях, переходим к следующей главе: 6. Fake vs mock vs stub.