Интерфейсы в Go

Теория: Интерфейсы для моков

Во время разработки код часто зависит от внешних источников: базы данных, сетевых сервисов, файловых систем. Такие зависимости сложно протестировать напрямую — они медленные, нестабильные и требуют настройки. Чтобы изолировать бизнес-логику и проверить её поведение, используют моки — поддельные реализации интерфейсов. Они ведут себя как настоящие зависимости, но работают локально, предсказуемо и быстро.

Чтобы заменить реальную зависимость на мок, в коде должна быть абстракция. В Go такой абстракцией служит интерфейс. Если функция зависит не от конкретного типа, а от интерфейса, можно передать любую реализацию — в том числе тестовую заглушку. Это позволяет проверить логику независимо от внешнего окружения. Допустим, есть функция, которая сохраняет пользователя в хранилище. В простейшем варианте она может выглядеть так:

func SaveUser(id string) error {
	return db.Save(id, []byte("some data"))
}

Тестировать такую функцию неудобно. Она жёстко привязана к базе данных. Чтобы упростить тест, достаточно выделить интерфейс:

type Storage interface {
	Save(key string, data []byte) error
}
func SaveUser(s Storage, id string) error {
	return s.Save(id, []byte("some data"))
}

Теперь в тестах можно передать подмену:

type mockStorage struct {
	Called   bool
	SavedKey string
}
func (m *mockStorage) Save(key string, data []byte) error {
	m.Called = true
	m.SavedKey = key
	return nil
}

И сам тест:

func TestSaveUser(t *testing.T) {
	mock := &mockStorage{}
	err := SaveUser(mock, "123")
    	if err != nil {
		t.Fatal("не ожидалось ошибки")
	}
	if !mock.Called || mock.SavedKey != "123" {
		t.Errorf("ожидался вызов Save с ключом 123, а был: %v", mock.SavedKey)
	}
}

Такой способ называют ручным мокированием. Он удобен, когда интерфейс небольшой и содержит один-два метода. В более сложных случаях код заглушек становится громоздким. Для решения таких проблем создали библиотеки, такие как Mockery.

Mockery автоматически генерирует моки на основе интерфейсов. Интерфейс описывается один раз, после чего команда генерации создаёт подменную реализацию. Она включает инструменты для задания ожиданий вызовов, аргументов и проверки порядка/кратности. Чтобы начать, установите генератор:

go install github.com/vektra/mockery/v2@latest

Затем сгенерируйте мок (пример для интерфейса Storage), используя типобезопасный expecter-API, максимально похожий на EXPECT() из GoMock:

mockery \
  --name=Storage \
  --srcpkg=yourproject/storage \
  --output=yourproject/storage/mocks \
  --outpkg=mocks \
  --filename=mock_storage.go \
  --with-expecter

В тестах отдельный контроллер не нужен — конструктор мока привязывается к *testing.T и сам повесит AssertExpectations через t.Cleanup(). Пример:

package storage_test
import (
	"testing"
    	"yourproject/storage"
	"yourproject/storage/mocks"
    	"github.com/stretchr/testify/mock"
)
func TestSaveUser(t *testing.T) {
	// Создаём мок, привязанный к T (автопроверка ожиданий на выходе из теста)
	mockStorage := mocks.NewMockStorage(t)
    	// Типобезопасные ожидания через expecter-API (похоже на gomock.EXPECT())
	mockStorage.EXPECT().
		Save("123", mock.Anything).
		Return(nil)
        	err := storage.SaveUser(mockStorage, "123")
	if err != nil {
		t.Fatal("не ожидалось ошибки")
	}
}

Метод EXPECT() задаёт ожидаемые вызовы и аргументы. Если ожидание не будет выполнено (вызвано с другими аргументами, не было вызвано вовсе или вызвано неверное число раз), тест упадёт на проверке ожиданий.

Альтернатива: без --with-expecter можно использовать стиль Testify:

mockStorage.On("Save", "123", mock.Anything).Return(nil).Once()

Но expecter-API ближе к привычному синтаксису из GoMock.

Автоматическое мокирование особенно полезно в больших проектах, где интерфейсы содержат десятки методов: оно избавляет от рутины, делает тесты чище и снижает риск ошибок. Благодаря структурной типизации Go достаточно, чтобы тип соответствовал методам интерфейса, — и его можно подменить. Поэтому интерфейсы в Go критичны для тестируемого кода.

Рекомендуемые программы

+7 800 100 22 47

бесплатно по РФ

+7 495 085 21 62

бесплатно по Москве

108813 г. Москва, вн.тер.г. поселение Московский,
г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3
ОГРН 1217300010476
ИНН 7325174845