Ответ
Существует три основных подхода для тестирования кода с зависимостью от БД, каждый со своими плюсами и минусами:
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), чтобы тесты не влияли друг на друга.
Ответ 18+ 🔞
А, ну вот, опять про тестирование с этой ёбнутой базой данных! Слушай, тут, блядь, как обычно, три пути, и все они ведут в пизду, но по-разному. Выбирай, на чём подорваться.
1. Настоящая база, но в банке (Testcontainers)
Представь, ты для каждого теста, как дурак, запускаешь целый докер-контейнер с постгресом. Это как приглашать на чай экскаватор — мощно, но дохуя мороки.
- Чем хорошо, блядь:
- Всё по-чесноку: Тестируешься на той же самой хуйне, что и в бою. Никаких сюрпризов.
- Чистота, ёпта: Каждый тест начинает с девственной базы. Никаких чужих соплей.
- Чем пиздецово:
- Медленно, как черепаха в сиропе: Ждать, пока контейнер встанет, — это отдельная медитация.
- Зависишь от докера: Нет докера — нихуя не протестируешь.
// Вот так это выглядит, сука
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) // После теста — в утиль
// ... дальше подключаешься и тестишь как с настоящей...
}
2. Показательные выступления (Моки)
А это уже для юнит-тестов, где надо проверить логику, а не реальную базу. Ты создаёшь интерфейс для своего репозитория, а потом в тестах подсовываешь муляж, который делает вид, что он база.
- Плюсы, ёбана:
- Быстрее ветра: Всё летает в оперативке, никаких тормозов.
- Король положения: Заставишь мок вернуть любую хуйню или упасть с ошибкой — полный контроль.
- Минусы, пиздец:
- SQL не проверяется вообще: Ты можешь написать в коде
SELECT * FROM users WHERE хуй = 1, и мок это проглотит, а в продакшене всё ебнется. - Хрупкое говно: Изменил сигнатуру метода в реальном коде — иди и переписывай все моки, блядь.
- 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"}
// Настраиваем спектакль: "Когда спросят юзера с ID=1, верни вот этого"
mockRepo.On("GetUserByID", 1).Return(user, nil)
service := NewUserService(mockRepo)
// ... и тестируем сервис, который думает, что ходит в базу...
}
3. База-пустышка (In-memory, типа SQLite)
Компромисс, ёпта. Берёшь SQLite, запускаешь её в памяти и гоняешь на ней тесты. Быстро и запросы настоящие.
- Чем прикольно:
- Шустро: Быстрее контейнеров, не надо ждать.
- SQL работает: Запросы-то выполняются, синтаксис проверяется.
- Чем опасно:
- Диалекты, блядь: SQLite — это не PostgreSQL. То, что работает на SQLite, может нахуй послать тебя на продакшене с его постгресом. Идеальный способ получить ложное чувство безопасности.
Так как же жить-то, ёпта?
- Двумя руками, блядь: Моки — для быстрых юнит-тестов твоей бизнес-логики (сервисов, хендлеров). Testcontainers — для медленных, но честных интеграционных тестов, которые проверяют, что твой репозиторий вообще умеет разговаривать с реальной базой.
- Дели на слои: Всю работу с базой засунь в отдельный слой (репозиторий). Его будет легко замокать для всего, что выше.
- Убирай за собой: В интеграционных тестах оборачивай всё в транзакцию и делай
ROLLBACKв конце. Чтобы тесты не насрали друг другу в базу и не начали ругаться.