Контракты интерфейсов и 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 и реальной реализации при одинаковой семантике.
Типовые вопросы
- Нужно ли объявлять интерфейс для каждого сервиса?
- Нет. Он нужен у потребителя при реальной вариативности или изоляции эффекта; конкретный тип часто проще и честнее.
- Почему интерфейс должен быть малым?
- Меньше связность, проще fake и меньше риск сломать реализации при эволюции API.
- Когда mock хуже fake?
- Когда тест проверяет implementation details вместо результата и становится хрупким при рефакторинге.
- Где размещать интерфейс?
- Обычно рядом с кодом, который его использует; реализация не должна навязывать широкий интерфейс всем потребителям.
- Как тестировать HTTP-клиент?
- Unit-тестировать policy через малый интерфейс/
RoundTripper, а формат и совместимость дополнительно проверять черезhttptestили integration test.
- Unit-тестировать policy через малый интерфейс/
Практика
- Напишите
fakeSenderи тесты успеха/ошибки дляWelcome. - Сократите искусственный интерфейс из пяти методов до потребности одного use case.
- Создайте contract test, который выполнится для in-memory fake и production repository.
Критерии готовности: тесты не зависят от порядка нерелевантных вызовов, зависимости видны в конструкторе, fake сохраняет существенную семантику (например, ErrNotFound), а go test -race ./... проходит при параллельных тестах.
Частые ошибки и ловушки
- Принимать
interface{}или огромный interface «на будущее». - Генерировать mock до определения поведенческого контракта.
- Прятать зависимость в package global и подменять её из параллельных тестов.
- Считать mock доказательством работы с реальной БД, сетью или схемой протокола.
Связанные темы
Go-код на интервью · Публичный API пакета · Тестирование
Источники
- Go blog: The Laws of Reflection — раздел об интерфейсах, проверено 2026-10-02.
- Package io — примеры малых capability-интерфейсов, проверено 2026-10-02.
- Go Wiki: TableDrivenTests — проверено 2026-10-02.