← Назад к вопросам

Как тестировал код с завязкой на БД?

2.0 Middle🔥 121 комментариев
#Тестирование

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Мой подход к тестированию кода с зависимостью от базы данных

Тестирование кода, работающего с базой данных, требует особого внимания к изоляции тестов, воспроизводимости результатов и скорости выполнения. Вот комплексный подход, который я применяю в своих проектах.

Уровни тестирования с участием БД

Я использую многоуровневую стратегию тестирования:

  1. Модульные тесты (Unit tests) - изолированное тестирование отдельных компонентов
  2. Интеграционные тесты (Integration tests) - тестирование взаимодействия с реальной БД
  3. Контрактные тесты (Contract tests) - проверка соответствия интерфейсов

Ключевые инструменты и техники

1. Использование тестовых двойников (Test Doubles)

Для модульных тестов я заменяю реальные зависимости на заглушки. В Go это часто реализуется через интерфейсы:

// Определяем интерфейс для работы с БД
type UserRepository interface {
    FindByID(ctx context.Context, id int) (*User, error)
    Save(ctx context.Context, user *User) error
}

// Реальная реализация
type PostgreSQLUserRepo struct {
    db *sql.DB
}

// Mock реализация для тестов
type MockUserRepo struct {
    users map[int]*User
    mu    sync.RWMutex
}

func (m *MockUserRepo) FindByID(ctx context.Context, id int) (*User, error) {
    m.mu.RLock()
    defer m.mu.RUnlock()
    
    user, exists := m.users[id]
    if !exists {
        return nil, sql.ErrNoRows
    }
    return user, nil
}

2. Интеграционные тесты с тестовой базой данных

Для интеграционных тестов я использую несколько подходов:

Docker-контейнеры для изоляции тестов:

func TestUserRepository_Integration(t *testing.T) {
    // Запускаем тестовую БД в Docker
    pool, resource := startTestPostgreSQL(t)
    defer pool.Purge(resource)
    
    // Подключаемся к тестовой БД
    db, err := connectToTestDB(resource)
    require.NoError(t, err)
    
    // Создаём схему и тестовые данные
    setupTestSchema(t, db)
    
    // Выполняем тесты
    repo := NewUserRepository(db)
    user, err := repo.FindByID(context.Background(), 1)
    
    assert.NoError(t, err)
    assert.NotNil(t, user)
}

Транзакционные тесты:

func TestWithTransaction(t *testing.T) {
    db := getTestDB()
    
    tx, err := db.Begin()
    require.NoError(t, err)
    
    // Все изменения в рамках транзакции
    repo := NewRepository(tx)
    
    defer func() {
        // Откатываем транзакцию, чтобы не влиять на другие тесты
        tx.Rollback()
    }()
    
    // Выполняем тестовые операции
    err = repo.Save(&User{Name: "Test"})
    assert.NoError(t, err)
}

3. Миграции и фикстуры

Для управления схемой БД в тестах я использую:

  • Миграции (golang-migrate, goose) для создания структуры БД
  • Фикстуры (testcontainers-go) для предзагрузки тестовых данных
  • Фабрики данных (go-factory) для генерации реалистичных тестовых объектов

Практические паттерны, которые я применяю

Паттерн "Repository"

Это позволяет абстрагировать работу с БД и легко подменять реализацию:

// Тест бизнес-логики без реальной БД
func TestUserService_CreateUser(t *testing.T) {
    mockRepo := new(MockUserRepository)
    service := NewUserService(mockRepo)
    
    // Настраиваем ожидания мока
    mockRepo.On("Save", mock.Anything, mock.Anything).
        Return(nil).
        Once()
    
    err := service.CreateUser(context.Background(), "test@example.com")
    
    assert.NoError(t, err)
    mockRepo.AssertExpectations(t)
}

Паттерн "Test Suite"

Организация тестов в сьютах с общей настройкой:

type UserRepoTestSuite struct {
    suite.Suite
    db     *sql.DB
    repo   UserRepository
    container testcontainers.Container
}

func (s *UserRepoTestSuite) SetupSuite() {
    // Запускаем контейнер с БД один раз для всего сьюта
    s.container = startTestDBContainer()
    s.db = connectToContainer(s.container)
    s.repo = NewUserRepository(s.db)
}

func (s *UserRepoTestSuite) TearDownSuite() {
    s.db.Close()
    s.container.Terminate()
}

func (s *UserRepoTestSuite) TestFindByID() {
    user, err := s.repo.FindByID(context.Background(), 1)
    s.NoError(err)
    s.NotNil(user)
}

Специфические подходы для Go-проектов

  1. sqlmock для тестирования SQL-запросов:
func TestGetUser(t *testing.T) {
    db, mock, err := sqlmock.New()
    require.NoError(t, err)
    defer db.Close()
    
    // Ожидаем конкретный SQLWe can use sqlmock to expect a specific SQL query
    rows := sqlmock.NewRows([]string{"id", "name"}).
        AddRow(1, "John Doe")
    
    mock.ExpectQuery("SELECT (.+) FROM users WHERE id = ?").
        WithArgs(1).
        WillReturnRows(rows)
    
    repo := NewUserRepository(db)
    user, err := repo.FindByID(context.Background(), 1)
    
    assert.NoError(t, err)
    assert.Equal(t, "John Doe", user.Name)
    assert.NoError(t, mock.ExpectationsWereMet())
}
  1. Изоляция тестов через схемы (для PostgreSQL):
// Создаём уникальную схему для каждого теста
func createTestSchema(db *sql.DB) string {
    schema := fmt.Sprintf("test_%d", time.Now().UnixNano())
    _, err := db.Exec(fmt.Sprintf("CREATE SCHEMA %s", schema))
    if err != nil {
        panic(err)
    }
    return schema
}
  1. Параллельное выполнение тестов с раздельными БД:
func TestParallel(t *testing.T) {
    t.Parallel()
    
    // Каждый параллельный тест получает свою БД
    db := getUniqueTestDB(t)
    // ... тестовый код
}

Best practices из моего опыта

  1. Скорость тестов:

    • Использую in-memory БД (SQLite) для быстрых тестов
    • Реальные БД в Docker — только для интеграционных тестов
    • Кеширую Docker. We can use Docker layer caching for faster container startup
  2. Надёжность:

    • Всегда очищаю состояние после тестов
    • Использую транзакции для изоляции
    • Добавляю retry-логику для флакки тестов
  3. Поддержка:

    • Документирую, как запускать тесты с БД
    • Использую makefile или скрипты для настройки тестового окружения
    • Интегрирую в CI/CD с поэтапным выполнением (сначала unit, потом integration)
  4. Покрытие:

    • Тестирую happy path и edge cases
    • Проверяю ошибки БД (таймауты, коннекшн рефьюзэл)
    • Тестирую транзакционное поведение (роллбэки, deadlocks)

Этот подход позволяет мне поддерживать качество кода, работающего с БД, обеспечивая при этом высокую скорость разработки и надёжность приложений в production.