Содержание урока · 10 разделов
- Заглушка и мок: разница внутри одного mock.Mock
- Times, Once, Twice: из намерения в требование
- Лишний вызов: что происходит сверх Times
- AssertCalled, AssertNotCalled, AssertNumberOfCalls
- Инструменты проверки вызовов на одном экране
- Правила проверки вызовов
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Профиль перестал сохраняться после правок валидации, а тест оставался зелёным: заглушка из урока про 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
GOtype 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) в конце теста?
История с ревью релиза: сервис регистрации после правок стал выходить раньше, чем вызывал Save, и тест оставался зелёным два дня — assert.NoError был доволен, а заглушка молчала в углу. В проде ушли ноль регистраций: тест проверял возврат, а не факт вызова. Чинили тест, а не сервис — добавили AssertExpectations последней строкой, и объявленное On("Save", 42) перестало быть декорацией. Вторая половина той же ловушки — рукописный фейк без mock.Mock: он на любой вызов молча отдаёт нулевые значения, и зелёный цвет означает ровно ничего. mock.Mock хотя бы падает на незаявленном вызове, а сверка ожиданий закрывает дыру «вызова не было вовсе».
Лишний вызов: что происходит сверх 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.
Тест ловит двойной вызов
GOtype 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 на пачке объектов. В чистом виде шаблон умещается в несколько строк вокруг уже знакомых блоков:
Три строки шаблона целиком
GOfunc 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("Send", msg).Return(nil).Times(2) и сервис, который должен повторять попытку при сбое. Тест горел зелёным: лишний вызов сломал бы прогон, а его не было. Потом перекроили обработчик ошибок — ретрай исчез совсем, вызов остался один, и тест продолжил быть зелёным месяц: недобор проверяет только AssertExpectations, которого не было. Пользователи месяц не получали повторных писем. Мораль: Times — половина контракта, вторая половина живёт в сверке ожиданий в конце теста; одна половина без второй не работает.
Правила проверки вызовов#
- Настроил — выполнил — сверил: On в начале теста, AssertExpectations в последней строке.
- Times, Once и Twice без сверки проверяют только превышение, но не недобор.
- Лишний вызов сверх счётчика роняет тест сразу, на строчке вызова.
- Мок без привязки Test(t) вместо аккуратного провала паникует с тем же текстом.
- AssertNotCalled — сторож запрещённых вызовов, AssertNumberOfCalls — грубый счёт.
- Пустой мок проходит AssertExpectations: ноль из нуля — не проверка.
Соберите тест с проверкой вызовов
Допишите объявление ожидания и финальную сверку вызовов.
Соберите шаблон настроил — выполнил — сверил
Расставьте строки: ожидание, вызов, проверка результата и финальная сверка ожиданий.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: проверка вызовов мока
Что дальше#
Проверка вызовов закрыла триаду 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) с двойным вызовом и увидите сообщение о вызове сверх счётчика.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем AssertExpectations отличается от AssertCalled?
AssertExpectations сверяет весь список ожиданий разом и требует, чтобы каждое сработало нужное число раз. AssertCalled проверяет один конкретный факт — был ли вызов метода с такими аргументами. На практике сверка ожиданий закрывает тест, а AssertCalled и AssertNotCalled работают точечно внутри, например как сторож на запрещённый вызов.
Почему мок паникует вместо аккуратного провала теста?
Мок, созданный структурным литералом, ничего не знает о testing.T, и сообщить о проблеме он может только паникой. Привяжите тест вызовом Test(t) или берите конструктор mockery NewMockXxx(t): тогда сбой идёт через t.Errorf и FailNow, а текст сообщения остаётся тем же самым.
Нужен ли AssertExpectations в каждом тесте с моком?
Не обязательно. Для читательских заглушек, где важен только возврат, сверка ожиданий добавляет шум и хрупкость. Для side-effect-зависимостей — отправка, сохранение, удаление — проверка вызовов и есть часть контракта, и сверка в финале обязательна. Граница простая: если тесту всё равно, был ли вызов, сверять нечего.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- mockery: генерация моков из интерфейсов
- Тестирование HTTP-хендлеров с testify
- assert.Eventually: тесты асинхронного кода
- JSONify и сравнение JSON в тестах
- Аргументы вызова: Anything и свои matcher'ы
- On и Return: настраиваем поведение заглушек
- Знакомство с mock.Mock: первый мок
- Табличные тесты с testify
- Все курсы по Go
Следующая ступень учебного пути
Viper: конфигурация приложений на Go
Интерактивный курс Viper для Go: YAML и env-переменные, приоритеты источников, Unmarshal, WatchConfig, профили окружений и секреты. 25 уроков с примерами.
- Собирать конфиг из файлов, окружения и флагов в один реестр
- Читать значения типизированно: Get, GetString, Unmarshal в структуры
- Задавать приоритеты источников: флаг > env > файл > default
- Перечитывать конфиг на лету через WatchConfig и OnConfigChange