---
title: "On и Return: настраиваем поведение заглушек"
description: "stub.On и Return в testify: сценарии ответов для разных аргументов, Return по сигнатуре метода, счётчики Once и Times, эффекты через Run и правила матчинга."
course: "Testify: тестирование Go с assert и require"
url: "https://golangmaster.ru/courses/testify/lessons/15-mock-on-return/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# On и Return: настраиваем поведение заглушек

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

Связка stub.On и Return — то, что превращает заглушку из пустышки в управляемого дублёра зависимости: mock on return настраивает поведение заглушек по сценариям теста. Разберём ответы для разных аргументов, сигнатуру Return, счётчики Once и Times, побочные эффекты через Run и правила выбора ожидания, когда подходят несколько.

Прошлый урок закончился на том, что [mock.Mock](/courses/testify/lessons/14-mock-znakomstvo/) принимает вызовы и запоминает их. Осталась вторая половина: научить заглушку отвечать. Связка stub.On и Return в пакете mock — способ сказать: когда у тебя спросят Get с id 42, верни Anna и nil, а с id 7 — ErrNotFound. Именно так настраивается поведение заглушек: ожидание и ответ склеены в одну цепочку, читающуюся как сценарий теста. Подмена возможна потому, что зависимость спрятана за интерфейсом — приём из [урока про интерфейсы](/courses/go-advanced/lessons/10-interfejsy/): сервис знает метод, а кто за ним стоит — решает тест. Прогоняет всё go test, как разобрано в [тестировании продвинутого курса](/courses/go-advanced/lessons/16-testirovanie/). Дальше по порядку: ответы для разных аргументов, сигнатура Return, счётчики Once и Times, эффекты через Run и правила, по которым testify выбирает ожидание, когда подходят несколько.

### stub.On и Return: связка ожидание-ответ

Любая настройка начинается с вызова stub.On(метод, аргументы...). Метод — строка с именем, аргументы — то, с чем вызов обязан случиться: On("Get", 42) означает «ждать Get с сорока двумя». Цепочка возвращает строитель Call, у которого и берётся Return: .Return(user, nil). Пока вызова нет, ожидание просто лежит в моке; как только мок-метод делает m.Called(42), testify ищет подходящее On и отдаёт заготовленный ответ. Из этого устройства следуют три правила. Аргументы матчатся точно: 43 не подойдёт к On("Get", 42), а вызов вовсе без подходящего ожидания паникует с сообщением I don't know what to return. Return без On не бывает: сначала объявляете, что ждёте, потом — что вернёте. И настройка идёт до вызова тестируемого кода: после svc.Profile(42) менять сценарий поздно, поздние On повлияют только на следующие вызовы.

### Первый сценарий: Get возвращает пользователя

```go
type User struct {
        ID   int
        Name string
}

type UserRepo struct{ mock.Mock }

func (m *UserRepo) Get(id int) (*User, error) {
        args := m.Called(id)
        user, _ := args.Get(0).(*User) // comma-ok: nil от заглушки не паникует
        return user, args.Error(1)
}

func TestProfileFound(t *testing.T) {
        repo := new(UserRepo)
        anna := &User{ID: 42, Name: "Anna"}

        repo.On("Get", 42).Return(anna, nil)

        svc := NewProfileService(repo)
        got, err := svc.Profile(42)

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

Мок встраивает mock.Mock, метод распаковывает ответ по позициям: args.Get(0) — первый параметр, args.Error(1) — второй. Запятая-ok при приведении типа оставляет user равным nil, когда заглушка вернула nil. Сценарий объявлен до вызова svc.Profile(42): On ждёт ровно 42 и отдаёт anna. Прогон — локальный go test: пакет mock не исполняется в браузерной песочнице.

### Несколько On на один метод: сценарии по аргументам

Один метод — сколько угодно ожиданий. On("Get", 42) и On("Get", 7) живут параллельно: на каждом вызове testify перебирает ожидания и берёт то, чьи аргументы совпали. Так записываются сценарии: счастливый путь, отсутствующая запись, отказ доступа — по ожиданию на ветку. Аргументы — единственный способ развести сценарии внутри одного метода, поэтому в них закладывают вход теста: id, который должен найтись, и id, который обязан уйти в ошибку. Если подходят несколько ожиданий, testify выбирает одно по своим правилам приоритета: в актуальной версии побеждает первое неисчерпанное в порядке объявления. Отсюда рабочий порядок «частные раньше общего»: сначала On("Get", 42) с конкретным ответом, затем On("Get", mock.Anything) — базовый ответ для всех остальных значений. Частный сценарий со счётчиком отвечает, пока счётчик жив, исчерпанный — уступает общий; а правило выбора между подходящими On менялось между версиями testify, так что порядок объявлений держите осознанным и одинаковым во всём наборе.

### Два сценария: найден и не найден

```go
var ErrNotFound = errors.New("user not found")

func TestProfileScenarios(t *testing.T) {
        repo := new(UserRepo)
        anna := &User{ID: 42, Name: "Anna"}

        repo.On("Get", 42).Return(anna, nil)
        repo.On("Get", 7).Return(nil, ErrNotFound)

        svc := NewProfileService(repo)

        got, err := svc.Profile(42)
        require.NoError(t, err)
        assert.Equal(t, "Anna", got.Name)

        _, err = svc.Profile(7)
        assert.ErrorIs(t, err, ErrNotFound)
}
```

Два On на один Get различаются только аргументом — и этого достаточно: 42 уходит в счастливый путь, 7 — в ErrNotFound. Проверку ошибки делаем ErrorIs: sentinel устойчив к обёрткам fmt.Errorf, которые сервис может добавить по дороге. Добавьте третий сценарий On("Get", 99) с другой ошибкой — набор расширится без правки старых ожиданий.

### Return повторяет сигнатуру метода

Return — не «что-то подходящее», а точная копия хвоста сигнатуры: столько же значений, в том же порядке и с теми же типами. Метод Get объявлен как (*User, error) — значит, Return ждёт два значения: указатель и ошибку. Внутри мок-метод распаковывает их по позициям: args.Get(0) — первый параметр, args.Error(1) — второй. Написать .Return(anna) без ошибки — оставить мок-метод без второго значения: при распаковке args.Error(1) тест упадёт паникой о нехватке аргументов. Перепутать порядок хуже: nil и ошибка меняются местами, счастливый путь внезапно «падает», а отчёт указывает не на Return, а на ветку сервиса — диагностика растягивается. Правило простое: скопируйте сигнатуру метода в Return слово в слово, включая nil-ы. Отдельный случай — методы без возвращаемых значений: для void-методов Return не нужен вовсе, одного On достаточно, но факт вызова такая заглушка всё равно запомнит и отдаст его при проверке вызовов.

### Проверка: сколько значений в Return

**Вопрос:** Метод мока объявлен как func (m *Repo) Find(id int) (Item, error). Какой Return ему нужен?

- .Return(Item{})
- ✅ .Return(item, nil)
- .Return(nil, item)
- Return не обязателен: testify сам подставит нулевые значения

Return копирует сигнатуру: два значения — результат и ошибка. Один Item оставит распаковку без второй позиции, а перепутанный порядок отдаст структуру там, где мок-метод ждёт ошибку. Return обязателен всякий раз, когда метод что-то возвращает.

> **Зелёный тест, чужие данные.** Реальная история с ревью: сервис обязан отдавать профиль строго его владельцу, тест мокал репозиторий через On("Get", mock.Anything).Return(anna, nil) и проверял Profile(999) — зелёный: мок вернул Anna и на несуществующий id. В бою репозиторий отдал запись чужого пользователя, и тест не мог этого поймать в принципе: заглушка подменяет репозиторий, а не проверяет его. Если сценарий про то, что вернёт зависимость, различайте аргументы: точный On на несуществующий id с ответом nil, ErrNotFound. Заглушка честна ровно настолько, насколько точно повторяет контракт — включая ответы на плохие входы.

### Once, Twice, Times: счётчик вызовов

У каждого ожидания есть счётчик: .Once() — ответить один раз, .Twice() — два, .Times(n) — ровно n. По умолчанию счётчика нет, и заглушка отвечает бессрочно. Зачем ограничивать? Первое — расходуемость: частный сценарий с Once отвечает ровно один заход, исчерпанный — уступает дорогу другому подходящему ожиданию; если запасного нет, testify роняет тест сообщением о превышении числа вызовов. Второе — читаемость: Once в тесте говорит «этот сценарий — ровно один заход», и следующий читающий понимает намерение без комментариев. Третье — строгость: Times(3) читается как «метод обязан вызваться трижды», и это требование проверяется в конце теста через AssertExpectations — тема отдельного урока, там же разберём, что случится, если счётчик не израсходован. В этом уроке берём из счётчиков расходуемость и намерение: полный контроль над числом вызовов приходит позже, вместе с явной проверкой ожиданий.

### Модификаторы ожидания на одном экране

| Метод | Что делает | Когда берём |
| --- | --- | --- |
| .Return(v, err) | Отдаёт значения при вызове | Всегда, когда метод возвращает значения |
| .Once() | Разрешает один вызов | Разовый сценарий внутри теста |
| .Twice() | Разрешает два вызова | Двойное срабатывание: ретрай, два запроса |
| .Times(n) | Разрешает n вызовов | Фиксированное число заходов |
| .Run(fn) | Побочный эффект при вызове | Записать вызов, закрыть канал, дернуть коллбэк |
| .Unset() | Снимает ожидание | Перенастроить заглушку между шагами теста |

### Run: побочный эффект на вызове

Return отвечает значением, Run — действием. Цепочка On("Delete", 42).Return(nil).Run(func(args mock.Arguments){ ... }) исполняет функцию всякий раз, когда срабатывает ожидание: аргументы приходят в args и читаются по позиции — args.Int(0), args.String(1), args.Error(2). Классика — собрать, что именно удалили: Run складывает id в слайс, тест в конце сверяет порядок. Так же пишут эффекты, которые нельзя выразить значением: закрыть канал, дернуть коллбэк, инкрементировать счётчик, отправить сообщение второй горутине. Честности ради: Mock и сам записывает каждый вызов в поле Calls — часто аргументы можно достать и без Run, просто прочитав m.Calls[0].Arguments после действия. Run берут, когда эффект нужен в момент вызова: заполнять слайс тестируемого кода, эмулировать запись в файл, срабатывать синхронно с обращением. Порядок в цепочке свободный: Return и Run можно менять местами, testify учтёт оба.

### Run пишет удаления в слайс

```go
func (m *Cache) Evict(id int) error {
        args := m.Called(id)
        return args.Error(0)
}

func TestCacheEvicts(t *testing.T) {
        cache := new(Cache)

        var evicted []int
        cache.On("Evict", mock.Anything).
                Return(nil).
                Run(func(args mock.Arguments) {
                        evicted = append(evicted, args.Int(0))
                })

        svc := NewCleanupService(cache)
        svc.Purge([]int{10, 20, 30})

        assert.Equal(t, []int{10, 20, 30}, evicted)
}
```

Evict отвечает nil через nil-безопасный args.Error(0), а Run в момент каждого вызова складывает id в evicted. Матчер mock.Anything пропускает любой ключ — содержимое мы всё равно фиксируем в слайсе и сверяем через assert.Equal. Для многоразовой заглушки со сбором вызовов это короче, чем On на каждый id.

### Проверка: два подходящих ожидания

**Вопрос:** Ожидания настроены в этом порядке: сначала On("Get", mock.Anything).Return(guest, nil), затем On("Get", 42).Return(anna, nil). Что вернёт repo.Get(42) в актуальной testify?

- anna: частный On точнее — testify предпочитает конкретику
- ✅ guest: testify возьмёт первое подходящее неисчерпанное ожидание в порядке объявления
- nil, nil: два On на один метод конфликтуют
- Паника: два On на один метод запрещены

Матчатся оба, но testify берёт первое неисчерпанное в порядке объявления — Anything объявлен раньше. Частный сценарий объявляйте до общего: On("Get", 42) первым, Anything последним. Порядок — часть настройки, и в старых версиях testify правило выбора работало наоборот.

> **Спор: сценарные моки против фиктивной реализации.** Моё правило, в котором я далеко не одинок: до двух-трёх сценариев на метод берите On, дальше пишите фиктивную реализацию. Пять On с ветками превращают тест в интерпретатор условий: читающий восстанавливает, какое ожидание победит и почему. Фиктивный репозиторий с картой внутри отвечает по данным: data[42] — и сценарий виден сразу, плюс он переиспользуется тремя сервисами сразу. Условие, при котором я прав: интерфейс маленький и стабильный. Для одного теста с одним ответом мок короче — фиктивная реализация ради единственного возврата — церемония ради церемонии.

### Правила матчинга, которые помнит testify

- Аргументы матчатся точно: тип и значение по порядку — 43 не подойдёт к On с 42.
- Вызов без подходящего On паникует: mock: I don't know what to return because the method call was unexpected.
- Подходят несколько ожиданий — в актуальной версии побеждает первое неисчерпанное в порядке объявления.
- Исчерпанный счётчик Once или Times уступает дорогу другому подходящему On, а без запасного тест падает с сообщением о превышении вызовов.
- Частные сценарии объявляйте раньше общего Anything-ответа — иначе общий перехватит их вызовы.
- On объявляют до вызова тестируемого кода: после запуска менять сценарий поздно.

### Что решит прогон?

```go
func TestProfileMissing(t *testing.T) {
        repo := new(UserRepo)
        repo.On("Get", 42).Return(&User{ID: 42, Name: "Anna"}, nil)

        svc := NewProfileService(repo)

        _, err := svc.Profile(43)

        assert.Error(t, err)
}
```

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

- Тест зелёный: err равен nil
- ✅ Тест красный: паника mock: I don't know what to return
- Тест красный: Get вернул Anna, а assert.Error ждёт ошибку
- Тест зелёный: testify сам подставит ErrNotFound

Profile(43) зовёт Get(43), а объявлен только On на 42 — совпадения нет, и testify паникует с I don't know what to return. Никаких молчаливых нулевых значений: незаявленный вызов — ошибка настройки, и отчёт говорит об этом прямо, с подсказкой ближайшего ожидания.

### Соберите сценарий заглушки

Допишите настройку заглушки и проверку результата: объявление ожидания, ответ и сверку имени.

```go
func TestProfile(t *testing.T) {
        repo := new(UserRepo)
        repo.___("Get", 42).___(&User{ID: 42, Name: "Anna"}, nil)

        svc := NewProfileService(repo)

        got, err := svc.Profile(42)

        require.NoError(t, err)
        assert.___(t, "Anna", got.Name)
}
```

Подсказка: Ожидание объявляет On, ответ отдаёт Return, сверку строки делает Equal.

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

### Соберите тест с двумя сценариями

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

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

1. `repo := new(UserRepo)`
2. `repo.On("Get", 42).Return(anna, nil)`
3. `repo.On("Get", 7).Return(nil, ErrNotFound)`
4. `svc := NewProfileService(repo)`
5. `_, err := svc.Profile(7)`
6. `assert.ErrorIs(t, err, ErrNotFound)`

### Карточки: On и Return

- **stub.On(метод, аргументы...)** — Объявляет ожидание: метод с такими аргументами должен быть вызван. Возвращает строитель Call для настройки ответа.
- **.Return(v...)** — Ответ заглушки: значения в порядке и количестве сигнатуры метода; для void-методов не нужен.
- **.Once() / .Twice() / .Times(n)** — Счётчик вызовов ожидания: частный сценарий отвечает, пока счётчик жив, затем уступает общий On.
- **.Run(func(args mock.Arguments))** — Побочный эффект при срабатывании: собрать вызовы в слайс, закрыть канал. Аргументы читаются по позиции: args.Int(0), args.Get(1).
- **Приоритет среди подходящих On** — В актуальной testify — первое неисчерпанное в порядке объявления: частные сценарии раньше общего Anything-ответа.
- **Вызов без подходящего On** — Паника mock: I don't know what to return — сценарий забыли объявить до вызова тестируемого кода.

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

Заглушка отвечает по сценариям — остался вопрос: с чем её вызывали? Этим займётся следующий урок про аргументы вызова: точные значения, mock.Anything, AnythingOfType и свои условия через MatchedBy. А объявленные здесь Times и Once обретут строгую проверку в уроке про AssertExpectations: факт вызова и его число станут требованием, а не намерением. Примеры урока — тестовые пакеты: они запускаются локальным go test, браузерная песочница их не исполняет, зато весь набор прогоняется за секунды. Возьмите за привычку добавлять к своим мокам по одному сценарию после каждого урока — к концу курса у вас будет живой справочник. Карта всех уроков — на странице [testify-курса](/courses/testify/).

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

- stub.On(метод, аргументы).Return(...) — связка ожидание и ответ: заглушка отвечает только на заявленный вызов.
- Несколько On на один метод — сценарии: различаются аргументами, побеждает первое неисчерпанное ожидание в порядке объявления.
- Return копирует сигнатуру метода: столько же значений, в том же порядке; ошибка идёт последним значением.
- Once, Twice и Times задают счётчик: исчерпанное ожидание уступает общий On или роняет тест при превышении.
- Run исполняет побочный эффект в момент вызова: собрать вызовы в слайс, закрыть канал.
- Вызов без подходящего On паникует: mock: I don't know what to return.
- До двух-трёх сценариев — On, дальше фиктивная реализация интерфейса читается проще.

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

Возьмите сервис с зависимостью-интерфейсом и напишите три теста на одном моке: счастливый путь с точным id, отсутствующая запись с ErrNotFound и Times(2) с Run, собирающим вызовы в слайс. Затем добавьте четвёртый тест с вызовом необъявленного id и посмотрите на панику I don't know what to return в отчёте.

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

### Что вернёт заглушка, если On объявлен без Return?

Зависит от распаковки в методе мока: сгенерированные стабы достают значения comma-ok формой и отдают нулевые — nil, ноль, пустую ошибку. Но полагаться на умолчание не советую: явный Return убирает вопросы у читающего и защищает от паники при строгой распаковке.

### Чем Run отличается от записи в m.Calls?

Mock и так запоминает каждый вызов в поле Calls — аргументы можно достать после действия. Run нужен, когда эффект должен случиться в момент вызова: заполнить слайс тестируемого кода, отправить в канал, эмулировать запись. Для простой инспекции аргументов Calls обычно достаточно.

### Почему подошло несколько On и сработало не то ожидание?

В актуальной testify побеждает первое неисчерпанное подходящее ожидание в порядке объявления, в старых версиях — последнее. Переставьте объявления или дайте частному сценарию Once или Times: общий On с Anything объявляйте после частных, и поведение станет предсказуемым.
