Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 18 из 25 ~18 мин +20 XP

mockery: генерация моков из интерфейсов

Рукописный мок на два экрана — первая плата за mock.Mock, и mockery отменяет этот налог: генерация моков из интерфейсов по одному конфигу занимает секунды. Ставим инструмент, описываем .mockery.yaml, зовём NewMockXxx и EXPECT() в тестах, перегенерируем при смене интерфейса — и честно разберём случаи, когда рукописный фейк до сих пор лучше.

Содержание урока · 10 разделов

Интерфейс UserStore рос весь квартал: Get, Save, Delete, Rename — и вместе с ним разросся рукописный мок: структура с mock.Mock, по методу-распаковщику на каждый, m.Called, args.Get, comma-ok — два экрана кода, скопированных почти без изменений из урока про On и Return. Когда добавили Delete, мок правили в четырёх местах и в одном опечатались. mockery снимает эту рутину: генератор читает интерфейс из пакета и выпускает готовый мок для testify/mock — с конструктором, expecter-стилем и встроенной сверкой ожиданий из урока про AssertExpectations. Здесь — установка, конфиг .mockery.yaml, то, что именно генерируется, обновление при смене интерфейса и честная граница: когда рукописный фейк всё-таки лучше.

Установка и .mockery.yaml: packages-конфиг#

Ставится mockery как бинарник: go install github.com/vektra/mockery/v2@latest — или brew install mockery; версию стоит фиксировать, потому что формат генерации между релизами меняется. Конфиг лежит в .mockery.yaml в корне репозитория — mockery ищет его вверх от текущей папки. Актуальный стиль описания — секция packages: путь к пакету, внутри — interfaces с перечнем интерфейсов; всё остальное (all: true, go:generate и конфиги без packages) официально устарело с версии 2.31.0 и в третьей версии уйдёт совсем — про саму v3 с обновлённым форматом документация предупреждает отдельно, миграция не мгновенная. Запуск без аргументов: команда mockery читает конфиг и генерирует всё заявленное. Повторный запуск безопасен, но безжалостен: файлы перезаписываются целиком — отсюда ниже правило про ручные правки.

Минимальный .mockery.yaml

GO
with-expecter: true

packages:
  github.com/example/service/store:
    config:
      recursive: true
    interfaces:
      UserStore:
      SessionStore:
        config:
          with-expecter: false

Верхний with-expecter включает expecter-стиль для всех моков сразу; interface-секция может выключить его точечно. Имена под interfaces без тела означают «генерировать только их»; recursive: true распространяет конфиг на подпакеты. По умолчанию моки падают в mocks/<путь пакета>/mock_<Интерфейс>.go. Прогон — локальный Go: mockery ставится go install и запускается командой mockery из корня проекта.

Что генерируется: MockUserStore, NewMockUserStore и EXPECT()#

Для интерфейса UserStore появится тип MockUserStore, встроивший mock.Mock, — тот же механизм, что в рукописных моках. Первое отличие — конструктор NewMockUserStore(t): он привязывает testing.T к моку, из-за чего сбои идут аккуратным FailNow вместо паники, и регистрирует AssertExpectations через t.Cleanup — сверка из прошлого урока выполняется сама, последнюю строку писать не нужно. Второе — expecter: метод EXPECT() возвращает MockUserStore_Expecter, у которого методы интерфейса принимают аргументы обычным кодом: m.EXPECT().Get(42).Return(anna, nil) — и опечатка в имени здесь ошибка компиляции, а не тихо неисполненное ожидание. Возвраты настраиваются на Call-объекте вида MockUserStore_Get_Call: Return, Run, RunAndReturn — прямой аналог цепочек из пятнадцатого урока. Смешивать стили можно: сгенерированный мок — это тот же mock.Mock, и старый On("Get", 42) сработает и с ним.

Тест с сгенерированным моком

GO
import (
        "testing"

        "github.com/example/service/mocks"
)

func TestProfileFound(t *testing.T) {
        m := mocks.NewMockUserStore(t)
        anna := &User{ID: 42, Name: "Anna"}

        m.EXPECT().Get(42).Return(anna, nil).Once()

        svc := NewProfileService(m)

        got, err := svc.Profile(42)

        require.NoError(t, err)
        assert.Equal(t, anna, got)
}

Четыре строки настройки против двух экранов рукописного мока. Конструктор NewMockUserStore(t) уже привязал t и повесил AssertExpectations на t.Cleanup — потому финальной сверки в тесте нет. Expecter-цепочка читается как фраза: жду Get с 42, верни anna один раз. Прогон — локальным go test: тестовый пакет с импортом mocks песочница не запускает.

Проверка: конструктор

Чем mocks.NewMockUserStore(t) отличается от &mocks.MockUserStore{}?

Старый On и новый expecter: какой стиль живёт в тестах#

Сгенерированный мок встраивает mock.Mock, поэтому оба стиля вызовов работают одновременно: классический On("Get", 42).Return(anna, nil) из прошлых уроков и типизированный expecter m.EXPECT().Get(42).Return(anna, nil). Выбирать лучше на старте проекта: expecter строже — имена методов и число аргументов проверяет компилятор; On гибче в старых кодовых базах, где строки и матчеры уже написаны. mock.Anything и MatchedBy работают в обоих стилях, нюанс документации такой: аргументы expecter-методов объявлены interface{}, чтобы принять mock.Anything на любой позиции — строгая типизация есть у имён методов, а не у значений. Счётчики одинаковы: Once и Times(n) вешаются на Call в обеих формах. Мой рабочий выбор — expecter по умолчанию, On там, где тесту нужен матч по предикату в одну строку; результат одинаковый, различие в том, кто ловит ошибки раньше.

RunAndReturn: ответ из функции

GO
m := mocks.NewMockUserStore(t)

m.EXPECT().Get(mock.Anything).
        RunAndReturn(func(id int) (*User, error) {
                if id == 42 {
                        return &User{ID: 42, Name: "Anna"}, nil
                }
                return nil, ErrNotFound
        })

RunAndReturn принимает функцию с сигнатурой метода: ответ вычисляется из входа, а не фиксирован. Один expecter-метод покрывает оба сценария — найденного и пропавшего пользователя, — вместо двух On-строк. Для побочных эффектов без возврата остаётся Run, для фиксированных ответов — обычный Return.

Что вернёт expecter с RunAndReturn?

Прочитайте код и выберите, что он выведет:

m := mocks.NewMockUserStore(t)

m.EXPECT().Get(mock.Anything).
        RunAndReturn(func(id int) (*User, error) {
                return &User{ID: id, Name: "user"}, nil
        })

got, err := m.Get(7)

fmt.Println(got.ID, err == nil)

Проверка: вызов мимо ожидания

Мок создан через mocks.NewMockUserStore(t), объявлено m.EXPECT().Get(42).Return(anna, nil). Сервис зовёт m.Get(7). Что произойдёт?

Рукописный мок против mockery#

КритерийРукописный мокmockery
Новый метод интерфейсадописать распаковщик рукамиперегенерация одной командой
Опечатка в имени методастрока в On, компилятор молчитошибка компиляции expecter
Проверка ожиданийAssertExpectations рукамиконструктор ставит t.Cleanup сам
Объём кода в репозиторииэкраны копипастыодин yaml и сгенерированные файлы
Сценарии по даннымOn-строка на каждый случайRunAndReturn с функцией
Зависимости проектатолько testifytestify плюс инструмент генерации

Обновление моков при смене интерфейса#

Смене интерфейса соответствует простая процедура: правите интерфейс — запускаете mockery — чините красное. Добавили метод Delete: перегенерённый мок его получит, а тесты, где сервис зовёт Delete без EXPECT, упадут с unexpected call — это сигнал объявить ожидание. Переименовали Get в Fetch: старые m.EXPECT().Get(...) перестают компилироваться — красное на этапе сборки, раньше любого прогона. Удалили метод: лишние ожидания в тестах становятся лишним кодом, компилятор укажет где. На этой петле mockery и окупается: рукописный мок при переименовании молча расходится с интерфейсом, пока кто-то не заметит вручную. В CI держите перегенерацию проверкой: прогнать mockery и убедиться, что git diff по mocks/ чист, — дрейф конфига и версии генератора виден в пайплайне, а не у новичка на второй неделе.

Генерация — не догма, и у меня есть сильное предпочтение в обратную сторону для маленьких интерфейсов: один-два метода, сценарии по данным — рукописный фейк с картой внутри читается как документация и живёт без тулчейна. Фейк data[42] вместо expecter-цепочки понятен любому, он переиспользуется тремя пакетами сразу и не требует, чтобы каждый участник команды держал mockery в своей цепочке сборки. Граница, где я сдаюсь и генерирую: три метода и больше, интерфейс, растущий вместе с сервисом, и моки в десятках тестов — там копипаста и расхождения при смене контракта стоят дороже, чем один yaml и команда. Середина здравая: сгенерированные моки на швах с внешними системами, рукописные фейки на внутренних контрактах, которые меняются редко.

Рабочий процесс с mockery#

  • Зафиксируйте версию mockery и держите её одинаковой у всех и в CI.
  • Опишите .mockery.yaml с секцией packages и with-expecter: true — один конфиг на репозиторий.
  • После смены интерфейса запускайте mockery и разбирайте красные тесты.
  • В тесте: NewMockXxx(t), затем EXPECT() с Return, Once или RunAndReturn.
  • AssertExpectations не пишите: конструктор выполняет её через t.Cleanup.
  • Сгенерированные файлы не редактируйте: перегенерация перезаписывает их целиком.

Соберите тест с сгенерированным моком

Допишите конструктор мока, вход в expecter и фиксированный возврат.

func TestProfile(t *testing.T) { m := mocks.(t) m.().Get(42).(anna, nil) svc := NewProfileService(m) got, err := svc.Profile(42) require.NoError(t, err) assert.Equal(t, anna, got) }

Соберите сценарий с expecter

Расставьте строки теста: конструктор, ожидание, вызов сервиса и проверки.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: mockery

Термин

mockery

нажмите, чтобы перевернуть
Определение

Генератор моков для testify/mock: читает интерфейсы и выпускает Mock-типы с конструктором и expecter-стилем.

1 / 6

Что дальше#

Моки закрыты: заглушки настроены, вызовы проверены, генерация автоматизирована. Дальше по курсу — тестирование HTTP-хендлеров: testify работает там в паре с httptest из стандартной библиотеки, и сгенерированные моки встают на шов между хендлером и хранилищем. После — Eventually для асинхронных тестов и границы testify против чистого testing. Примеры урока — тестовые пакеты: в песочнице их не запустить, полный прогон занимает секунды в терминале. Из практики: начните с одного интерфейса, а не с all: true — первые сгенерированные моки покажут, какой стиль вызовов приживётся в вашем проекте, прежде чем конфиг накроет весь репозиторий. База под генерацию — интерфейсы продвинутого курса, карта уроков — на странице testify-курса.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • mockery генерирует моки для testify/mock из интерфейсов: меньше копипасты, единый конфиг.
  • .mockery.yaml с секцией packages — актуальный стиль конфигурации; go:generate и конфиги без packages устарели с v2.31.0.
  • Конструктор NewMockXxx(t) привязывает testing.T и сам вызывает AssertExpectations через t.Cleanup.
  • EXPECT() даёт типизированные ожидания: опечатку в имени метода ловит компиляция, а не прогон.
  • On-стиль и expecter живут рядом: сгенерированный мок — тот же mock.Mock.
  • Смена интерфейса — это перегенерация и разбор красного: unexpected call или ошибка компиляции.
  • Один-два метода и сценарии по данным — место рукописного фейка, а не генерации.

Домашнее задание

Возьмите интерфейс Store с Get и Save, опишите .mockery.yaml с секцией packages и with-expecter, перегенерируйте мок. Перепишите один тест с рукописного на NewMockXxx и EXPECT и убедитесь, что AssertExpectations проходит сама через t.Cleanup. Добавьте в интерфейс метод Delete, перегенерируйте снова и разберите красный тест: unexpected call требует нового EXPECT.

Личные заметкиПусто
Хранятся только в вашем браузере (Testify · mockery: генерация моков из интерфейсов)

Вопросы и ответы по уроку

Какая версия mockery описана в уроке и что с v3?

Здесь v2 — та, что живёт в большинстве репозиториев: секция packages появилась в версии 2.31.0, expecter — в 2.10.0. Третья версия использует обновлённый формат конфигурации, и документация рекомендует миграцию, но переезд не мгновенный. Для проекта на v2 материал применим целиком — меняется только часть синтаксиса конфига.

Куда mockery кладёт сгенерированные файлы и можно ли рядом с интерфейсом?

По умолчанию — mocks/<путь пакета>/mock_<Интерфейс>.go, тип называется Mock<Интерфейс>. Генерацию можно переключить рядом с исходником через dir с шаблоном интерфейсной папки и inpackage: True — это убирает циклические импорты, но документация предупреждает: перегенерация перезаписывает файл целиком, ручные правки не сохраняются.

Что если сервис вызывает метод без EXPECT — тест упадёт или вернёт нули?

Упадёт: несматчеванный вызов — сбой в момент вызова, а не тихие нулевые значения. Если ожиданий на метод нет вовсе, сообщение — I don't know what to return; если ожидание есть, но аргументы другие — Unexpected Method Call с ближайшим. У мока из NewMockXxx(t) — аккуратный FailNow: testing.T привязан конструктором.

Термины урока

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

Следующая ступень учебного пути

Viper25 уроков~9 чпрактика25 уроков уже доступны

Viper: конфигурация приложений на Go

Интерактивный курс Viper для Go: YAML и env-переменные, приоритеты источников, Unmarshal, WatchConfig, профили окружений и секреты. 25 уроков с примерами.

  • Собирать конфиг из файлов, окружения и флагов в один реестр
  • Читать значения типизированно: Get, GetString, Unmarshal в структуры
  • Задавать приоритеты источников: флаг > env > файл > default
  • Перечитывать конфиг на лету через WatchConfig и OnConfigChange