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

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

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

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

AssertExpectations: проверяем, что мок вызван

AssertExpectations — та половина пакета mock, о которой забывают: проверка вызовов мока отличает мок от простой заглушки и ловит тесты, зелёные случайно. Разберём разницу stub и mock, Times и Once как требования, поведение при лишнем вызове и шаблон «настроил — выполнил — сверил», после которого зелёный тест действительно значит, что код сработал.

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

Профиль перестал сохраняться после правок валидации, а тест оставался зелёным: заглушка из урока про mock.Mock была настроена верно, только звать её никто не стал. Тест проверял отсутствие ошибки — и её не было. Это слепая зона любой заглушки: она отвечает на вызовы, но не гарантирует, что вызовы случились. Проверка вызовов мока — вторая половина работы с пакетом mock: связка On и Return объявляет намерения, а AssertExpectations в конце теста сверяет их с фактом. Разница простая: заглушка отвечает, мок ещё и проверяет. В testify оба поведения живут в одном типе mock.Mock, и включать проверку — ваше решение, а не свойство библиотеки. Дальше: Times и Once как требования, что происходит при лишнем вызове и правило, при котором зелёный тест действительно зелёный.

Заглушка и мок: разница внутри одного mock.Mock#

Терминология здесь путается постоянно, поэтому зафиксирую так, как удобно в работе. Заглушка — это поведение: On("Get", 42).Return(anna, nil) и всё, что вернётся сервису. Мок — это ожидания: те же On-строки, прочитанные в другом направлении — «метод Get обязан быть вызванным с 42», плюс проверки в конце теста. В testify разница не в типах, а в использовании: mock.Mock хранит два списка — ожидания и журнал фактических вызовов — и вы сами решаете, сверять ли одно с другим. Проверка вызовов нужна тогда, когда взаимодействие и есть контракт: письмо отправлено, платёж списан один раз, файл удалён только из закрытого доступа. Для чистых функций-чтецов проверка вызовов чаще шум: сервису позволено сходить в кэш или не сходить, ответ от этого не меняется. Отсюда рабочее правило: side-effect-зависимости проверяем через AssertExpectations, читательские заглушки — по возврату.

Первый тест с AssertExpectations

GO
type UserRepo struct{ mock.Mock }

func (m *UserRepo) Get(id int) (*User, error) {
        args := m.Called(id)
        user, _ := args.Get(0).(*User)
        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)

        repo.AssertExpectations(t)
}

Три части читаются как сценарий: объявили ожидание, выполнили действие, сверили. AssertExpectations пробегает по всем On-строкам мока и требует, чтобы каждая сработала — здесь Get с 42 случился, и проверка молчит. Строка стоит в конце не по прихоти: сначала собираем факты вызовов, потом сравниваем с планом. Прогон — локальным go test: тестовые пакеты песочница не исполняет.

Times, Once, Twice: из намерения в требование#

В уроке про On и Return счётчики работали как расходуемость: частный сценарий отвечал, пока счётчик жив, затем уступал общий. С AssertExpectations у них появляется вторая роль — требование. Times(3) теперь означает: ожидание обязано сработать ровно трижды. Пока третьего вызова нет, сверка покрасит тест отчётом вида FAIL: 0 out of 1 expectation(s) were met — коду, который вы тестируете, недостаёт вызова. Ожидание без счётчика мягче: достаточно одного срабатывания, повторные не мешают. Once — это Times(1), Twice — Times(2): сумма становится контрактом и для максимума, и для минимума. Отсюда неудобная правда: связка On с Times без AssertExpectations бесполезна наполовину — превышение счётчика роняет тест сразу, а недобор виден только при явной сверке в конце. Забыли сверку — проверили максимум, но не минимум.

Проверка: что значит ожидание без Times

Ожидание объявлено как On("Get", 42).Return(anna, nil), счётчика нет. Что потребует repo.AssertExpectations(t) в конце теста?

Лишний вызов: что происходит сверх Times#

Теперь обратная сторона. Ожидание с Times(1) сработало, счётчик исчерпан, а сервис зовёт метод ещё раз. На этом вызове testify не находит живого ожидания и уронит выполнение сообщением: assert: mock: The method has been called over 1 times. Either do one more Mock.On(...).Return(...), or remove extra call. Заметьте: это не отчёт AssertExpectations в конце, а остановка в момент вызова — тест краснеет на той строчке, где случился лишний заход. Формулировка зависит от ситуации: если для метода вообще нет подходящего ожидания, сообщение другое — mock: I don't know what to return because the method call was unexpected; когда ожидание есть, но аргументы мимо, testify покажет Unexpected Method Call и ближайшее ожидание с разбором различий. Нюанс из исходников: у мока, созданного литералом new(UserRepo), сбой — это паника; привяжите testing.T через mock.Test(t) — и лишний вызов станет аккуратным провалом через FailNow.

Тест ловит двойной вызов

GO
type Gate struct{ mock.Mock }

func (m *Gate) Open(id int) error {
        args := m.Called(id)
        return args.Error(0)
}

func TestOpenOnce(t *testing.T) {
        gate := new(Gate)

        gate.On("Open", 42).Return(nil).Times(1)

        svc := NewDoorService(gate)

        svc.OpenDoor(42) // первый вызов: ожидание живо
        svc.OpenDoor(42) // второй: остановка over 1 times
}

Times(1) разрешает ровно один заход: первый вызов съедает счётчик, второй упирается в исчерпанное ожидание, и mock роняет тест сообщением The method has been called over 1 times — прямо на строчке лишнего вызова. До AssertExpectations дело не доходит. У мока без привязки Test(t) это паника с тем же текстом. Проверка — локальным go test: увидите и точную строку, и трейс.

AssertCalled, AssertNotCalled, AssertNumberOfCalls#

AssertExpectations довольно груб: сверяет все ожидания разом. Для точечных вопросов у Mock есть три проверки поверх журнала вызовов. AssertCalled(t, "Get", 42) требует, чтобы вызов с такими аргументами случился, и в отчёте показывает, чем тестовый код отвечал на самом деле. AssertNotCalled — сторож: AssertNotCalled(t, "Delete", 42) в тесте read-only пути ловит удаление, даже если заглушка вернула безобидные нули. AssertNumberOfCalls(t, "Get", 2) сверяет сумму вызовов метода — грубее Times, зато без матчинга аргументов. Моё спорное правило: по умолчанию AssertExpectations ставлю только на side-effect-моки, а читательские заглушки проверяю по возврату — иначе каждый рефакторинг добавляет лишние красные тесты там, где поведение не менялось. Граница, при которой правило не работает: командный проект с общими моками, где «вызван хотя бы раз» — часть контракта пакета; там честнее держать сверку всегда, чем обсуждать, где она нужна.

Что скажет лишний вызов?

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

func TestRetry(t *testing.T) {
        gate := new(Gate)

        gate.On("Open", 42).Return(nil).Times(1)

        svc := NewDoorService(gate)

        svc.OpenDoor(42)
        svc.OpenDoor(42)
}

Инструменты проверки вызовов на одном экране#

ИнструментЧто проверяетКогда берём
AssertExpectations(t)все On-строки: каждая сработала и выбрала свой счётчикфинал теста с side-effect-моком
AssertCalled(t, "Get", 42)вызов с такими аргументами случилсяконкретный факт вызова внутри теста
AssertNotCalled(t, "Delete", 42)вызова с такими аргументами не былосторож: read-only путь не трогает запись
AssertNumberOfCalls(t, "Get", 2)сумма вызовов метода равна двумгрубый счёт без матчинга аргументов
Times(n), Once, Twiceтребование к числу срабатываний ожиданияконтракт ретраев и разовых действий

Шаблон, который стоит закрепить: настроил — выполнил — сверил. On-строки в начале теста читаются как план, действие в середине собирает факты, AssertExpectations в последней строке закрывает цикл. Порядок важен: сверка до действия бессмысленна — ожидания ещё не выполнены, и тест ложно краснеет. Два тонких места. Первое: мок без единого ожидания проходит AssertExpectations — ноль из нуля считается успехом, поэтому «проверил и всё сошлось» и «забыл объявить» выглядят одинаково зелёно; если сервис обязан позвать зависимость, On обязан быть в тесте. Второе: сверка смотрит на свой мок, а у теста их бывает несколько — каждому свой вызов, либо AssertExpectationsForObjects на пачке объектов. В чистом виде шаблон умещается в несколько строк вокруг уже знакомых блоков:

Три строки шаблона целиком

GO
func TestRegistration(t *testing.T) {
        store := new(UserStore)

        store.On("Save", mock.Anything).Return(nil).Once()

        svc := NewRegisterService(store)

        require.NoError(t, svc.Register("anna", 29))

        store.AssertExpectations(t)
}

Одна On-строка с Once, действие и сверка ожиданий последней строкой. Once здесь не украшение: он превращает «сохранил хотя бы раз» в «сохранил ровно один раз», что для регистрации и есть контракт. mock.Anything допустим, потому что содержимое проверяется отдельным тестом через разбор m.Calls — приём из урока про аргументы.

Проверка: забытая сверка

В тесте объявлено On("Save", 42).Return(nil).Once(), но сервис из-за бага не вызвал Save. AssertExpectations в тесте нет. Чем закончится прогон?

Правила проверки вызовов#

  • Настроил — выполнил — сверил: On в начале теста, AssertExpectations в последней строке.
  • Times, Once и Twice без сверки проверяют только превышение, но не недобор.
  • Лишний вызов сверх счётчика роняет тест сразу, на строчке вызова.
  • Мок без привязки Test(t) вместо аккуратного провала паникует с тем же текстом.
  • AssertNotCalled — сторож запрещённых вызовов, AssertNumberOfCalls — грубый счёт.
  • Пустой мок проходит AssertExpectations: ноль из нуля — не проверка.

Соберите тест с проверкой вызовов

Допишите объявление ожидания и финальную сверку вызовов.

func TestProfile(t *testing.T) { repo := new(UserRepo) repo.("Get", 42).Return(anna, nil).() svc := NewProfileService(repo) got, err := svc.Profile(42) require.NoError(t, err) assert.Equal(t, anna, got) repo.(t) }

Соберите шаблон настроил — выполнил — сверил

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

Строки

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

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

Карточки: проверка вызовов мока

Термин

AssertExpectations(t)

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

Сверяет все On-строки мока с фактом: каждая сработала и исчерпала свой счётчик. Ставят последней строкой теста.

1 / 6

Что дальше#

Проверка вызовов закрыла триаду mock: настройка, ответы, контроль. Осталась рутинная часть — сами моки: рукописные структуры из уроков четырнадцатого и пятнадцатого растут с каждым методом интерфейса, и писать их руками надоедает к третьему. Дальше по курсу — mockery, который генерирует Mock-типы из интерфейсов по конфигу, даёт конструктор NewMockXxx(t) — он заодно привязывает тест и сам вызывает AssertExpectations через t.Cleanup, — и expecter-стиль EXPECT(), где имя метода проверяет компилятор, а не строка в On. Примеры урока — тестовые пакеты: песочница их не исполняет, полный прогон занимает секунды в терминале. Возьмите тест из домашки и добавьте к нему AssertNotCalled на метод, который сервис звать не должен, — сторож пригодится раньше, чем кажется. Карта всех уроков — на странице testify-курса.

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

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

Вывод

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

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

  • Заглушка отвечает на вызовы, мок проверяет их факт: в testify оба поведения живут в mock.Mock.
  • AssertExpectations сверяет все On-строки с журналом вызовов и ставится последней строкой теста.
  • Ожидание без Times требует хотя бы одного срабатывания; Times(n) — ровно n.
  • Лишний вызов сверх счётчика роняет тест сразу: The method has been called over N times.
  • Без привязки Test(t) сбой мока — паника; с привязкой — аккуратный FailNow.
  • AssertNotCalled — сторож запрещённых вызовов, AssertNumberOfCalls — грубый счёт.
  • Пустой мок и забытая сверка дают зелёный тест без единой проверки.

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

Возьмите сервис с side-effect-зависимостью и соберите шаблон: On с Return и Once, действие, AssertExpectations последней строкой. Сломайте сервис — пусть перестанет звать зависимость — и сравните прогон со сверкой и без: без неё тест зелёный, с ней краснеет. Добавьте Times(1) с двойным вызовом и увидите сообщение о вызове сверх счётчика.

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

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

Чем AssertExpectations отличается от AssertCalled?

AssertExpectations сверяет весь список ожиданий разом и требует, чтобы каждое сработало нужное число раз. AssertCalled проверяет один конкретный факт — был ли вызов метода с такими аргументами. На практике сверка ожиданий закрывает тест, а AssertCalled и AssertNotCalled работают точечно внутри, например как сторож на запрещённый вызов.

Почему мок паникует вместо аккуратного провала теста?

Мок, созданный структурным литералом, ничего не знает о testing.T, и сообщить о проблеме он может только паникой. Привяжите тест вызовом Test(t) или берите конструктор mockery NewMockXxx(t): тогда сбой идёт через t.Errorf и FailNow, а текст сообщения остаётся тем же самым.

Нужен ли AssertExpectations в каждом тесте с моком?

Не обязательно. Для читательских заглушек, где важен только возврат, сверка ожиданий добавляет шум и хрупкость. Для side-effect-зависимостей — отправка, сохранение, удаление — проверка вызовов и есть часть контракта, и сверка в финале обязательна. Граница простая: если тесту всё равно, был ли вызов, сверять нечего.

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

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

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

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

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

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

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