Ответ
Существует три основных подхода для тестирования кода с зависимостью от БД, каждый со своими плюсами и минусами:
1. Использование реальной БД в Docker (Testcontainers)
Это наиболее надежный подход для интеграционного тестирования. Библиотека testcontainers-go
программно запускает Docker-контейнер с нужной БД (PostgreSQL, MySQL и т.д.) для каждого тестового набора.
- Плюсы:
- Максимальный реализм: Тесты выполняются на той же версии и типе БД, что и в продакшене.
- Полная изоляция: Каждый тест (или пакет тестов) получает чистую, изолированную БД.
- Минусы:
- Медленно: Запуск контейнера занимает время.
- Зависимость: Требуется установленный и запущенный Docker.
// Пример с testcontainers-go
func TestUserRepository_WithPostgres(t *testing.T) {
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: "postgres:13-alpine",
ExposedPorts: []string{"5432/tcp"},
Env: map[string]string{"POSTGRES_PASSWORD": "password"},
}
pgContainer, _ := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{...})
defer pgContainer.Terminate(ctx)
// ... получаем connection string и пишем тесты ...
}
2. Использование Mock-объектов (Моки)
Этот подход используется для unit-тестирования. Вы создаете интерфейс для вашего слоя доступа к данным (например, UserRepository
) и в тестах подменяете его mock-реализацией, которая имитирует поведение БД.
- Плюсы:
- Очень быстро: Никаких внешних зависимостей, тесты выполняются в памяти.
- Полный контроль: Легко имитировать любые сценарии, включая ошибки БД.
- Минусы:
- Не тестирует SQL: Вы не проверяете корректность самих SQL-запросов и их совместимость с БД.
- Хрупкость: Моки могут устареть, если реальная логика работы с БД изменится.
// Пример с testify/mock
type MockUserRepository struct {
mock.Mock
}
func (m *MockUserRepository) GetUserByID(id int) (*User, error) {
args := m.Called(id)
return args.Get(0).(*User), args.Error(1)
}
func TestUserService(t *testing.T) {
mockRepo := new(MockUserRepository)
user := &User{ID: 1, Name: "Test"}
// Настраиваем мок: при вызове GetUserByID(1) вернуть user и nil-ошибку
mockRepo.On("GetUserByID", 1).Return(user, nil)
service := NewUserService(mockRepo)
// ... тестируем логику сервиса ...
}
3. Использование In-memory БД (например, SQLite)
Это компромиссный вариант. Для тестов используется легковесная, встраиваемая БД, которая работает в памяти.
- Плюсы:
- Быстро: Быстрее, чем Docker, не требует внешних зависимостей.
- Тестирует SQL: Выполняются реальные SQL-запросы.
- Минусы:
- Несоответствие диалектов: SQL-диалект SQLite может отличаться от вашей основной БД (например, PostgreSQL), что может привести к тестам, проходящим локально, но падающим в продакшене.
Лучшие практики:
- Комбинируйте подходы: Используйте моки для быстрых юнит-тестов бизнес-логики и testcontainers для интеграционных тестов, проверяющих реальное взаимодействие с БД.
- Слой доступа к данным: Выделяйте всю работу с БД в отдельный слой (Repository Pattern), который легко мокировать.
- Транзакции: Оборачивайте каждый интеграционный тест в транзакцию с последующим откатом (
ROLLBACK
), чтобы тесты не влияли друг на друга.