Go: Автоматическое тестирование

Теория: Мокирование зависимостей через интерфейсы

В любом более-менее серьёзном коде появляются зависимости. Это может быть база данных, HTTP-клиент, файловая система, кэш или внешнее API. В рабочем коде они нужны — но в тестах такие зависимости превращаются в проблему. Поднять настоящую базу? Медленно. Настроить внешний сервис? Ненадёжно, он может быть недоступен. А если тесты начинают реально стучаться в интернет или писать на диск — никакой изоляции уже нет.

Поэтому разработчики придумали подмену зависимостей: вместо реальной базы или API в тестах используется поддельная реализация. В Go это особенно удобно, потому что есть интерфейсы. Код работает через интерфейс, и ему всё равно, кто стоит «за кулисами» — настоящая реализация или подделка.

Эта подделка и есть мок. Он ведёт себя как настоящая зависимость снаружи, но внутри возвращает ровно те ответы, которые нужны тесту.

Интерфейс как контракт

В Go интерфейс — это обещание: «Я умею эти методы». Всё. Конкретно как — решает реализация.

Например, сервис пользователей.

// User — простая модель данных.
type User struct {
	ID   int
	Name string
}

// UserStorage — интерфейс, описывающий контракт.
// Он говорит: "я умею отдавать пользователя по id".
type UserStorage interface {
	GetUser(id int) (User, error)
}

// Service зависит не от базы, а от интерфейса.
// Ему всё равно, что там внутри: Postgres, Redis, JSON-файл или мок.
type Service struct {
	storage UserStorage
}

func NewService(storage UserStorage) *Service {
	return &Service{storage: storage}
}

// GetName получает имя пользователя по id.
func (s *Service) GetName(id int) (string, error) {
	user, err := s.storage.GetUser(id)
	if err != nil {
		return "", err
	}
	return user.Name, nil
}

Здесь Service вообще не знает, где лежат пользователи. Он работает только через интерфейс. Это и даёт возможность в тестах подсовывать ему «куклу» вместо настоящего хранилища.

Самый простой мок: жёстко зашитый ответ

Для старта можно сделать совсем простой мок:

// mockStorageAlways возвращает одного и того же пользователя всегда.
type mockStorageAlways struct{}

func (m *mockStorageAlways) GetUser(id int) (User, error) {
	return User{ID: id, Name: "TestUser"}, nil
}

Тест с таким моком:

func TestService_WithSimpleMock(t *testing.T) {
	service := NewService(&mockStorageAlways{})

	name, err := service.GetName(42)
	if err != nil {
		t.Fatal(err)
	}
	if name != "TestUser" {
		t.Errorf("получили %q, хотели %q", name, "TestUser")
	}
}

Здесь всё жёстко: любой id возвращает одного и того же юзера. Это примитивно, но иногда этого хватает, чтобы проверить «провода»: что Service вообще вызывает GetUser и достаёт имя.

Более гибкий мок: заранее заданные данные

Чаще нужен контроль над тем, какие id есть, а какие нет. Тогда мок хранит карту пользователей.

// mockStorage хранит заранее подготовленных пользователей.
type mockStorage struct {
	users map[int]User
}

func (m *mockStorage) GetUser(id int) (User, error) {
	if user, ok := m.users[id]; ok {
		return user, nil
	}
	return User{}, fmt.Errorf("user %d not found", id)
}

Тест с таким моком:

func TestService_WithPreparedMock(t *testing.T) {
	mock := &mockStorage{
		users: map[int]User{
			1: {ID: 1, Name: "Alice"},
			2: {ID: 2, Name: "Bob"},
		},
	}

	service := NewService(mock)

	// проверяем успешный сценарий
	name, err := service.GetName(1)
	if err != nil {
		t.Fatal(err)
	}
	if name != "Alice" {
		t.Errorf("получили %q, хотели %q", name, "Alice")
	}

	// проверяем ошибку
	_, err = service.GetName(99)
	if err == nil {
		t.Fatal("ожидали ошибку, но её нет")
	}
}

Теперь поведение контролируем полностью: хотим — возвращаем юзера, хотим — ошибку.

Мок как способ эмулировать сбои

Иногда нужно проверить, как сервис ведёт себя при сбоях зависимостей. Например, база «упала».

// mockStorageError всегда возвращает ошибку.
type mockStorageError struct{}

func (m *mockStorageError) GetUser(id int) (User, error) {
	return User{}, fmt.Errorf("database is down")
}

Тест:

func TestService_WithErrorMock(t *testing.T) {
	service := NewService(&mockStorageError{})

	_, err := service.GetName(1)
	if err == nil {
		t.Fatal("ожидали ошибку, но её нет")
	}
}

Таким образом можно проверить, что сервис правильно обрабатывает критические ситуации и не падает сам.

Более сложный пример: HTTP-клиент

Возьмём случай, когда сервис делает HTTP-запрос. В боевом коде он использует http.Client, но в тестах мы не хотим ходить в интернет. Как быть?

Определим интерфейс:

// HTTPDoer описывает поведение: "я умею делать запросы".
type HTTPDoer interface {
	Do(req *http.Request) (*http.Response, error)
}

type APIClient struct {
	httpClient HTTPDoer
}

func NewAPIClient(httpClient HTTPDoer) *APIClient {
	return &APIClient{httpClient: httpClient}
}

func (c *APIClient) FetchData(url string) (string, error) {
	req, _ := http.NewRequest("GET", url, nil)
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	return string(body), nil
}

Реальный код использует *http.Client, потому что у него есть метод Do. А в тесте можно подставить свой мок:

// mockHTTPClient возвращает заранее подготовленный ответ.
type mockHTTPClient struct {
	response string
}

func (m *mockHTTPClient) Do(req *http.Request) (*http.Response, error) {
	r := io.NopCloser(strings.NewReader(m.response))
	return &http.Response{StatusCode: 200, Body: r}, nil
}

Тест:

func TestAPIClient_FetchData(t *testing.T) {
	mock := &mockHTTPClient{response: "mocked data"}
	client := NewAPIClient(mock)

	data, err := client.FetchData("http://example.com")
	if err != nil {
		t.Fatal(err)
	}
	if data != "mocked data" {
		t.Errorf("получили %q, хотели %q", data, "mocked data")
	}
}

Теперь тесты работают без интернета, быстро и предсказуемо.

Итоги

В Go интерфейсы позволяют легко подменять реальные зависимости на моки. Код работает через контракт (UserStorage, HTTPDoer), и ему всё равно, что стоит за ним. В реальности это база или сеть, в тесте — простая структура с картой или жёстко зашитым ответом.

Моки дают изоляцию, скорость и контроль: можно проверить как успешные сценарии, так и ошибки или сбои. Это делает тесты надёжными, повторяемыми и безопасными.

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

+7 800 100 22 47

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

+7 495 085 21 62

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

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