Зачем потребитель зависимости в Go обычно объявляет собственный интерфейс для mock-тестов?
Потребитель объявляет минимальный интерфейс, который описывает только нужные ему операции зависимости. Это уменьшает связанность, упрощает создание mock-объекта и позволяет тесту проверять контракт потребителя, а не детали реализации конкретного сервиса.
В Go интерфейсы реализуются неявно: типу не нужно заранее объявлять намерение реализовать интерфейс. Поэтому интерфейс удобно определять рядом с кодом, который использует поведение, а не обязательно в пакете реализации.
Такой подход решает исходную проблему избыточной связанности между пакетами. Потребитель зависит от небольшого поведенческого контракта, а реализация может изменяться или заменяться без изменения тестов и API производителя.
Если потребитель использует большой интерфейс, объявленный пакетом реализации, его тестовый mock вынужден поддерживать множество ненужных методов. Любое расширение этого интерфейса может ломать mock-тесты, даже если используемая потребителем часть контракта не изменилась.
Обратная сторона слишком общего решения — интерфейс, созданный только ради теста и содержащий искусственные методы. Такой контракт скрывает реальные зависимости и может позволить mock-объекту моделировать поведение, которого потребитель фактически не требует.
Интерфейс следует размещать в пакете-потребителе и делать настолько маленьким, насколько позволяет его код. Например, если сервису нужен только поиск записи, ему не требуется зависеть от полного интерфейса хранилища с операциями сохранения, удаления и транзакциями.
Здесь service владеет контрактом Finder, потому что именно ему известно, какое поведение необходимо. Реальный тип хранилища автоматически подходит этому интерфейсу, а тест использует простой mock без реализации нерелевантных методов.
Контракт нужно выбирать по поведению, а не по структуре конкретного типа. Если интерфейс нужен нескольким потребителям, это ещё не означает, что его следует поднимать в пакет реализации: у потребителей могут быть разные минимальные требования.
Полезная дополнительная проверка — compile-time assertion в пакете реализации, если важно явно зафиксировать совместимость с интерфейсом потребителя. Но это создаёт зависимость реализации от пакета потребителя, поэтому её стоит применять осознанно, особенно чтобы не получить циклический импорт.
Сервис заказов зависел от интерфейса Repository, объявленного в пакете базы данных. В интерфейсе было двенадцать методов, хотя сервис использовал только Find и Save. После добавления метода миграции тестовый mock перестал компилироваться, хотя поведение сервиса не изменилось.
Рассматривались два варианта. Можно было расширить mock новым методом, но это сохраняло связанность с чужим большим интерфейсом и увеличивало стоимость будущих изменений. Можно было оставить интерфейс в пакете базы данных, но выделить отдельный маленький интерфейс — это уменьшало mock, однако потребитель всё ещё зависел бы от структуры API производителя.
Выбрали интерфейсы в пакете сервиса: OrderReader с Find и OrderWriter с Save. Реальный репозиторий стал неявно удовлетворять обоим контрактам, а тестовые doubles реализовали только необходимые операции. В результате изменение API базы данных больше не ломало тесты сервиса без изменения используемого поведения.
Нет. Он уменьшает техническую связанность, но не гарантирует реалистичное поведение mock. Mock может возвращать невозможные состояния, нарушать порядок жизненного цикла или игнорировать ошибки. Поэтому тесты должны задавать существенные сценарии контракта, а интеграционные тесты — проверять совместимость с реальной реализацией.
Нет. Абстракция оправдана, когда она выражает устойчивую границу поведения или действительно нужна для изоляции. Механическая генерация интерфейсов для каждого типа усложняет код и может скрыть простую зависимость. Иногда достаточно передать небольшую функцию, если зависимость сводится к одной операции.
Не обязательно потребитель. Если новая возможность не нужна текущему потребителю, его интерфейс менять не следует. Новый потребитель может объявить собственный интерфейс с дополнительным методом или использовать отдельную абстракцию. Это сохраняет принцип минимального контракта и не заставляет существующие mock-объекты реализовывать невостребованное поведение.