---
title: "mockery: генерация моков из интерфейсов"
description: "mockery для Go: генерация моков из интерфейсов, конфиг .mockery.yaml с packages, NewMockXxx и EXPECT(), обновление моков при смене интерфейса."
course: "Testify: тестирование Go с assert и require"
url: "https://golangmaster.ru/courses/testify/lessons/18-mockery/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

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

*Курс «Testify: тестирование Go с assert и require», урок 18 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/testify/lessons/18-mockery/*

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

Интерфейс UserStore рос весь квартал: Get, Save, Delete, Rename — и вместе с ним разросся рукописный мок: структура с mock.Mock, по методу-распаковщику на каждый, m.Called, args.Get, comma-ok — два экрана кода, скопированных почти без изменений из [урока про On и Return](/courses/testify/lessons/15-mock-on-return/). Когда добавили Delete, мок правили в четырёх местах и в одном опечатались. mockery снимает эту рутину: генератор читает интерфейс из пакета и выпускает готовый мок для testify/mock — с конструктором, expecter-стилем и встроенной сверкой ожиданий из [урока про AssertExpectations](/courses/testify/lessons/17-mock-expectations/). Здесь — установка, конфиг .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{}?

- ✅ Привязывает testing.T и регистрирует AssertExpectations через t.Cleanup
- Включает генерацию expecter-методов на лету
- Запускает mockery и перегенерирует мок
- Ничем: это сокращённая запись того же самого

Конструктор делает две вещи: регистрирует testing.T на mock.Mock — сбои без паники — и вешает AssertExpectations на t.Cleanup, чтобы сверка ожиданий прошла перед выходом из теста. Генерация — процесс сборки: из теста mockery не запускается и expecter-методы не включает.

> **Опечатка в строке On пережила ревью.** Кейс из 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: ответ из функции

```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?

```go
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)
```

**Что выведет программа?**

- ✅ 7 true
- 0 false
- Паника: вызов Get(7) мимо ожидания
- 42 true

RunAndReturn вычисляет ответ на месте: id 7 попадает в структуру, ошибка nil — печатается 7 true. Матчер mock.Anything пропускает любой id, и прямой вызов метода на моке работает так же, как через сервис. Функция обязана повторять сигнатуру метода: перепутанный тип уронит вызов.

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

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

- ✅ Тест упадёт сразу: mock: Unexpected Method Call с ближайшим ожиданием и разбором различий
- Паника: I don't know what to return because the method call was unexpected
- Мок вернёт нулевые значения, тест продолжится
- Ошибка компиляции: expecter запрещает вызовы мимо EXPECT

Ожидание на Get существует, но аргументы мимо — testify покажет Unexpected Method Call и ближайшее ожидание с разницей. Сообщение I don't know what to return достаётся вызову метода, для которого On-строк нет вовсе. Конструктор привязал t, поэтому вместо паники — аккуратный FailNow.

> **Ручные правки в сгенерированном файле.** Инженер добавил в сгенерированный 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 и фиксированный возврат.

```go
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)
}
```

Подсказка: Конструктор берёт t, вход в expecter — EXPECT, фиксированный ответ задаёт Return.

Правильные ответы в пропусках: `NewMockUserStore`, `EXPECT`, `Return`

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

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

Правильный порядок строк:

1. `m := mocks.NewMockUserStore(t)`
2. `m.EXPECT().Get(42).Return(anna, nil).Once()`
3. `svc := NewProfileService(m)`
4. `got, err := svc.Profile(42)`
5. `require.NoError(t, err)`
6. `assert.Equal(t, anna, got)`

### Карточки: mockery

- **mockery** — Генератор моков для testify/mock: читает интерфейсы и выпускает Mock-типы с конструктором и expecter-стилем.
- **.mockery.yaml** — Конфиг генерации: секция packages с путями пакетов и перечнем interfaces. Актуальна с v2.31.0; go:generate и старые конфиги устарели.
- **NewMockUserStore(t)** — Конструктор сгенерированного мока: привязывает testing.T и регистрирует AssertExpectations через t.Cleanup.
- **m.EXPECT().Get(42)** — Expecter-стиль: имя метода проверяет компилятор; аргументы объявлены interface{}, чтобы принять mock.Anything.
- **RunAndReturn(fn)** — Ответ вычисляет функция с сигнатурой метода; Run — побочные эффекты, Return — фиксированные значения.
- **Смена интерфейса** — Правка, перегенерация mockery, разбор красного: unexpected call — добавьте EXPECT, ошибка компиляции — обновите старые ожидания.

### Что дальше

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

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

- 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 привязан конструктором.
