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

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

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

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

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

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

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

Прошлый урок закончился на том, что mock.Mock принимает вызовы и запоминает их. Осталась вторая половина: научить заглушку отвечать. Связка stub.On и Return в пакете mock — способ сказать: когда у тебя спросят Get с id 42, верни Anna и nil, а с id 7 — ErrNotFound. Именно так настраивается поведение заглушек: ожидание и ответ склеены в одну цепочку, читающуюся как сценарий теста. Подмена возможна потому, что зависимость спрятана за интерфейсом — приём из урока про интерфейсы: сервис знает метод, а кто за ним стоит — решает тест. Прогоняет всё go test, как разобрано в тестировании продвинутого курса. Дальше по порядку: ответы для разных аргументов, сигнатура 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 ему нужен?

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?

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

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

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

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

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

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

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

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

Термин

stub.On(метод, аргументы...)

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

Объявляет ожидание: метод с такими аргументами должен быть вызван. Возвращает строитель Call для настройки ответа.

1 / 6

Что дальше#

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

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

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

Вывод

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

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

  • 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 в отчёте.

Личные заметкиПусто
Хранятся только в вашем браузере (Testify · On и Return: настраиваем поведение заглушек)

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

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

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

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

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

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

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

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

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

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

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

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

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

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