Когда реальность мешает: зачем нам моки и фикстуры в Go?
Представьте, что вы тестируете функцию, которая отправляет email-уведомление. Каждый запуск теста — это реальное письмо в почтовый ящик. Через десять тестов ваш ящик забит, а коллеги проклинают спам. Или хуже: функция обращается к внешнему API, которое платное или работает раз в сутки.
В мире Go (или Golang, как его часто называют) тестирование — это не просто галочка в чек-листе. Это фундамент надежности. Но чтобы тесты были быстрыми и независимыми, нам нужно изолировать код от внешнего мира. Именно здесь на сцену выходят mock-объекты (моки) и фикстуры (fixtures).
В этой статье мы разберем, как правильно подменять зависимости (мокать) и готовить данные (фикстуры) в Go. Вы узнаете, как писать тесты, которые не падают из-за сети, базы данных или времени суток. Поехали!
Что такое Mock и Fixture в контексте Go?
Mock-объекты: двойники для сложных систем
Mock-объекты — это программные "заглушки", которые имитируют поведение реальных компонентов. Вместо того чтобы вызывать настоящую базу данных или HTTP-сервис, мы подставляем объект, который возвращает заранее заданные ответы.
В Go моки особенно важны, потому что язык поощряет работу с интерфейсами. Если ваш код зависит от интерфейса, вы легко можете подменить реализацию на тестовую.
Фикстуры: подготовка сцены
Фикстуры (Fixtures) — это данные или состояние, которые должны быть установлены до начала теста. Например, создание временной базы данных, заполнение её тестовыми записями или настройка файловой системы.
В отличие от моков, которые подменяют поведение, фикстуры подготавливают окружение. Они отвечают на вопрос: "Что должно быть готово, чтобы тест мог стартовать?"
Пишем моки в Go: от простого к сложному
Самый простой способ: ручная реализация интерфейса
Go не требует специальных библиотек для создания моков. Если у вас есть интерфейс, вы можете написать его тестовую реализацию вручную. Это идеально для небольших проектов.
Допустим, у нас есть интерфейс для отправки уведомлений:
package notifier
type Notifier interface {
Send(to string, message string) error
}
// Реальная реализация (не для тестов)
type EmailNotifier struct{}
func (e *EmailNotifier) Send(to string, message string) error {
// Отправка реального email
return nil
}
В тестовом файле мы создаем мок:
package notifier
import "testing"
type MockNotifier struct {
Called bool
LastTo string
LastMsg string
}
func (m *MockNotifier) Send(to string, msg string) error {
m.Called = true
m.LastTo = to
m.LastMsg = msg
return nil
}
func TestSendAlert(t *testing.T) {
mock := &MockNotifier{}
// Используем mock в функции, которая принимает Notifier
SendAlert(mock, "user@test.com", "Hello!")
if !mock.Called {
t.Error("Метод Send не был вызван")
}
if mock.LastTo != "user@test.com" {
t.Errorf("Неверный получатель: %s", mock.LastTo)
}
}
Плюсы: полный контроль, никаких зависимостей.
Минусы: много шаблонного кода для сложных интерфейсов.
Используем testify/mock: мощно и удобно
Библиотека testify — стандарт де-факто для моков в Go. Она предоставляет гибкий фреймворк для создания моков с проверкой аргументов и количества вызовов.
Установка:
go get github.com/stretchr/testify
Пример с использованием testify/mock:
package user
import (
"testing"
"github.com/stretchr/testify/mock"
)
// Определяем интерфейс
type UserRepository interface {
GetUser(id int) (string, error)
}
// Создаем мок-структуру
type MockUserRepo struct {
mock.Mock
}
func (m *MockUserRepo) GetUser(id int) (string, error) {
args := m.Called(id)
return args.String(0), args.Error(1)
}
func TestGetUserName(t *testing.T) {
repo := new(MockUserRepo)
// Настраиваем поведение мока
repo.On("GetUser", 42).Return("John Doe", nil)
name, err := GetUserName(repo, 42)
if err != nil {
t.Fatal(err)
}
if name != "John Doe" {
t.Errorf("Ожидалось John Doe, получено %s", name)
}
// Проверяем, что метод был вызван именно с аргументом 42
repo.AssertExpectations(t)
}
testify позволяет проверять последовательность вызовов, возвращать разные значения при повторных вызовах и даже имитировать панику.
Mock для HTTP-запросов: httptest
Когда ваш код делает внешние HTTP-вызовы, не нужно поднимать реальный сервер. Стандартная библиотека Go предлагает пакет net/http/httptest.
package api
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestFetchData(t *testing.T) {
// Создаем тестовый сервер
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status": "ok"}`))
}))
defer server.Close()
// Передаем URL тестового сервера в нашу функцию
result, err := FetchData(server.URL)
if err != nil {
t.Fatal(err)
}
if result != "ok" {
t.Errorf("Ожидалось 'ok', получено '%s'", result)
}
}
httptest — это "родной" инструмент Go, который не требует сторонних библиотек. Он идеально подходит для тестирования клиентов к внешним API.
Фикстуры в Go: готовим данные для тестов
Временные файлы и директории
Если ваш код работает с файловой системой, используйте t.TempDir(). Начиная с Go 1.15, эта функция создает временную директорию, которая автоматически удаляется после теста.
func TestProcessFile(t *testing.T) {
dir := t.TempDir()
filePath := dir + "/test.txt"
// Создаем фикстуру — файл с тестовыми данными
os.WriteFile(filePath, []byte("hello world"), 0644)
result := ProcessFile(filePath)
if result != "HELLO WORLD" {
t.Errorf("Ожидалось 'HELLO WORLD', получено '%s'", result)
}
}
Фикстуры для базы данных
Тестирование с реальной базой данных — сложная тема. Самый быстрый способ — использовать SQLite в памяти для изоляции.
package database
import (
"database/sql"
_ "github.com/mattn/go-s