Содержание урока · 10 разделов
- Установка и .mockery.yaml: packages-конфиг
- Что генерируется: MockUserStore, NewMockUserStore и EXPECT()
- Старый On и новый expecter: какой стиль живёт в тестах
- Рукописный мок против mockery
- Обновление моков при смене интерфейса
- Рабочий процесс с mockery
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Интерфейс 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
GOwith-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) сработает и с ним.
Тест с сгенерированным моком
GOimport (
"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{}?
Кейс из Code Review: в тесте писали On("Uplate", 42) — пропущенная буква, компилятор молчит: для него это просто строка. Тест оставался зелёным, потому что сервис в этом сценарии не доходил до Update, а AssertExpectations в конце не было: несработавшее ожидание никого не смущало. Через месяц соседний тест дёрнул ту ветку — и падение искали от обратного. Expecter-стиль закрывает класс проблемы: m.EXPECT().Uplate(...) не компилируется вовсе, и ошибка всплывает на строке опечатки, а не через месяц. Со строковым On остаётся одно правило: AssertExpectations в конце — иначе опечатка живёт вечно.
Старый 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: ответ из функции
GOm := 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). Что произойдёт?
Инженер добавил в сгенерированный MockUserStore счётчик вызовов и хелпер для тестов — файл в mocks/ выглядел как обычный исходник. Через месяц обновили mockery, прогнали генерацию по конфигу — и файл перезаписался целиком: счётчик исчез, зависшие тесты не собирались, а виновника искали по git blame до конфига. Сгенерированный файл — артефакт: правки живут в конфиге, хелперы — в отдельном файле рядом, а после обновления версии генератора diff по mocks/ читают целиком. Документация прямо предупреждает: mockery не защищает исходники от перезаписи при генерации рядом с интерфейсом.
Рукописный мок против mockery#
| Критерий | Рукописный мок | mockery |
|---|---|---|
| Новый метод интерфейса | дописать распаковщик руками | перегенерация одной командой |
| Опечатка в имени метода | строка в On, компилятор молчит | ошибка компиляции expecter |
| Проверка ожиданий | AssertExpectations руками | конструктор ставит t.Cleanup сам |
| Объём кода в репозитории | экраны копипасты | один yaml и сгенерированные файлы |
| Сценарии по данным | On-строка на каждый случай | RunAndReturn с функцией |
| Зависимости проекта | только testify | testify плюс инструмент генерации |
Обновление моков при смене интерфейса#
Смене интерфейса соответствует простая процедура: правите интерфейс — запускаете 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 и фиксированный возврат.
Соберите сценарий с expecter
Расставьте строки теста: конструктор, ожидание, вызов сервиса и проверки.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: mockery
Что дальше#
Моки закрыты: заглушки настроены, вызовы проверены, генерация автоматизирована. Дальше по курсу — тестирование 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.
Личные заметкиПусто
Вопросы и ответы по уроку
Какая версия 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 привязан конструктором.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Тестирование HTTP-хендлеров с testify
- assert.Eventually: тесты асинхронного кода
- JSONify и сравнение JSON в тестах
- Фикстуры и тестовые данные: где их хранить
- AssertExpectations: проверяем, что мок вызван
- Аргументы вызова: Anything и свои matcher'ы
- On и Return: настраиваем поведение заглушек
- Знакомство с mock.Mock: первый мок
- Все курсы по Go
Следующая ступень учебного пути
Viper: конфигурация приложений на Go
Интерактивный курс Viper для Go: YAML и env-переменные, приоритеты источников, Unmarshal, WatchConfig, профили окружений и секреты. 25 уроков с примерами.
- Собирать конфиг из файлов, окружения и флагов в один реестр
- Читать значения типизированно: Get, GetString, Unmarshal в структуры
- Задавать приоритеты источников: флаг > env > файл > default
- Перечитывать конфиг на лету через WatchConfig и OnConfigChange