В mock-объекте после изменения интерфейса зависимость перестала компилироваться только в части тестов. Какую проблему предотвращает этот приём и почему ошибка возникает на этапе компиляции?
package service
type Store interface {
Save(string) error
Load(string) (string, error)
}
type MockStore struct{}
func (m *MockStore) Save(string) error { return nil }
var _ Store = (*MockStore)(nil)
Строка var _ Store = (*MockStore)(nil) — это проверка соответствия интерфейсу на этапе компиляции. Она немедленно сообщит, что MockStore не реализует Store, потому что в нём отсутствует метод Load.
Такой приём предотвращает устаревание mock-объекта после изменения интерфейса. Он проверяет только форму API — наличие методов с правильными сигнатурами — но не корректность поведения mock-объекта.
В Go интерфейсы реализуются неявно: типу не нужно явно объявлять, что он реализует интерфейс. Это уменьшает связанность кода, но дефект может обнаружиться не в месте изменения mock-объекта, а позже — при передаче его в функцию, принимающую интерфейс.
Явная compile-time-проверка стала распространённым идиоматическим способом сделать неявное соответствие видимым для компилятора и команды. Особенно полезна она для mock-объектов, адаптеров и тестовых реализаций интерфейсов.
Если интерфейс расширили методом Load, старый mock всё ещё может успешно компилироваться как отдельный тип. Ошибка проявится только в том тесте или production-коде, где *MockStore потребуется использовать как Store.
Это создаёт несколько рисков: обратная связь от компилятора задерживается, место исправления менее очевидно, а CI может обнаружить проблему только при сборке конкретного пакета или тестового сценария.
Выражение var _ Store = (*MockStore)(nil) объявляет переменную с идентификатором _, поэтому значение не используется. Чтобы присвоение было допустимым, указатель *MockStore обязан реализовать интерфейс Store.
Компилятор проверяет весь методный набор указателя. В примере отсутствует Load, поэтому сборка завершается ошибкой примерно такого смысла: *MockStore does not implement Store.
Нужно учитывать тип receiver. Если метод объявлен как func (m MockStore) Save(...), его имеют и MockStore, и *MockStore; если receiver — *MockStore, соответствие может быть только у указательного типа. Поэтому в assertion следует указывать именно тот тип, который реально передаётся в production-код.
Проверка не запускает методы и не подтверждает, что mock возвращает правильные значения, корректно считает вызовы или моделирует ошибки. Для этого нужны отдельные тесты самого mock-объекта либо проверки взаимодействия в тестируемом компоненте.
Минимальный корректный вариант после добавления метода выглядит так:
Преимущество подхода — ранняя и точная ошибка без runtime-издержек. Ограничение — необходимость поддерживать assertion рядом с реализацией; при большом числе интерфейсов это увеличивает небольшой объём шаблонного кода.
Команда изменила интерфейс Client, добавив метод Close. Production-адаптер реализовывал новый метод, а hand-written mock — нет. Часть тестов продолжала компилироваться, потому что mock использовался только как конкретный тип; ошибка появилась позднее в другом пакете.
Рассматривались два варианта. Можно было полагаться на места фактического присваивания интерфейсу, но это давало отложенную и непредсказуемую обратную связь. Можно было добавить compile-time assertion в каждый адаптер и mock; это добавляло по одной строке, зато сразу связывало реализацию с контрактом.
Выбрали второй вариант. После этого изменение интерфейса стало немедленно выявлять все устаревшие реализации при сборке затронутого пакета. Поведенческие проверки mock при этом оставили отдельно, поскольку assertion не заменяет unit-тесты.
Нет. Проверяется только статическое соответствие: имена методов, их параметры и возвращаемые типы. Mock может реализовать интерфейс, но всегда возвращать неверные данные, игнорировать аргументы или неправильно моделировать ошибки. Поведение проверяют тестами, ориентированными на контракт зависимости и сценарии взаимодействия.
*MockStore, а не MockStore?Методы с pointer receiver входят в методный набор *MockStore, но не входят в методный набор MockStore. Поэтому assertion должен отражать фактический тип, передаваемый в код: если зависимость получает указатель, проверяют (*MockStore)(nil). Нулевой указатель здесь не разыменовывается — он используется только как значение для compile-time-проверки.
Можно, но это менее надёжно. Такая передача проверит соответствие только в конкретном месте использования; если mock временно не используется, проблема останется незамеченной. Отдельная assertion делает проверку постоянной, локальной и независимой от покрытия тестами.