Контракты интерфейсов и mockability в Go

Зачем это на интервью

Интервьюер смотрит, можете ли вы изолировать внешний эффект без создания архитектуры ради теста. В Go интерфейс — описание требуемого поведения, а не обязательный слой над каждой структурой. Он особенно полезен на границе сети, часов, файловой системы, БД или недетерминированности.

Минимум для E4

Определяйте интерфейс в пакете-потребителе и включайте только используемые методы. Принимайте конкретный тип, пока нет реальной вариативности или тестовой границы. Различайте:

  • stub возвращает заранее подготовленные ответы;
  • fake — рабочая упрощённая реализация, например in-memory repository;
  • mock проверяет ожидаемые взаимодействия;
  • spy записывает вызовы для последующей проверки.

Для бизнес-правила предпочтительнее тестировать результат и эффект через fake/stub, а не последовательность внутренних вызовов.

package notification
 
import "context"
 
type Sender interface {
	Send(context.Context, string, string) error
}
 
type Service struct {
	sender Sender
}
 
func NewService(sender Sender) Service {
	return Service{sender: sender}
}
 
func (s Service) Welcome(ctx context.Context, email string) error {
	return s.sender.Send(ctx, email, "welcome")
}
 
type fakeSender struct {
	to, body string
	err      error
}
 
func (f *fakeSender) Send(_ context.Context, to, body string) error {
	f.to, f.body = to, body
	return f.err
}

fakeSender находится в _test.go в реальном проекте, если его не нужно разделять между тестами. Конструктор требует зависимость явно, поэтому нельзя получить service с неинициализированной отправкой незаметно.

Углубление для E5/Senior

Контракт должен описывать семантику, а не форму транспорта: timeout, отмена, идемпотентность, порядок, гарантии доставки, возможные ошибки. Не расширяйте существующий интерфейс без необходимости: это ломает все реализации. Вместо интерфейса «для всего» выделите capability (Reader, Writer, Clock, TokenVerifier).

Объясняйте цену mockability: тест, привязанный к числу вызовов SQL/HTTP, препятствует безопасному кэшу или batching. Для критичных интеграций добавьте contract/integration test с настоящим протоколом; fake не доказывает совместимость с vendor API.

Ключевые понятия

  • Structural typing — тип реализует интерфейс неявно, если имеет нужные методы.
  • Consumer-side interface — потребитель формулирует минимальную потребность.
  • Dependency injection — зависимость передают через конструктор/параметр, а не скрывают в global state.
  • Compile-time assertion: var _ Sender = (*smtpSender)(nil) полезен для публичной реализации, но не обязателен повсюду.
  • Contract test — один набор проверок, выполняемый для fake и реальной реализации при одинаковой семантике.

Типовые вопросы

  1. Нужно ли объявлять интерфейс для каждого сервиса?
    • Нет. Он нужен у потребителя при реальной вариативности или изоляции эффекта; конкретный тип часто проще и честнее.
  2. Почему интерфейс должен быть малым?
    • Меньше связность, проще fake и меньше риск сломать реализации при эволюции API.
  3. Когда mock хуже fake?
    • Когда тест проверяет implementation details вместо результата и становится хрупким при рефакторинге.
  4. Где размещать интерфейс?
    • Обычно рядом с кодом, который его использует; реализация не должна навязывать широкий интерфейс всем потребителям.
  5. Как тестировать HTTP-клиент?
    • Unit-тестировать policy через малый интерфейс/RoundTripper, а формат и совместимость дополнительно проверять через httptest или integration test.

Практика

  • Напишите fakeSender и тесты успеха/ошибки для Welcome.
  • Сократите искусственный интерфейс из пяти методов до потребности одного use case.
  • Создайте contract test, который выполнится для in-memory fake и production repository.

Критерии готовности: тесты не зависят от порядка нерелевантных вызовов, зависимости видны в конструкторе, fake сохраняет существенную семантику (например, ErrNotFound), а go test -race ./... проходит при параллельных тестах.

Частые ошибки и ловушки

  • Принимать interface{} или огромный interface «на будущее».
  • Генерировать mock до определения поведенческого контракта.
  • Прятать зависимость в package global и подменять её из параллельных тестов.
  • Считать mock доказательством работы с реальной БД, сетью или схемой протокола.

Связанные темы

Go-код на интервью · Публичный API пакета · Тестирование

Источники