Базы данных — это фундамент большинства приложений, хранящие критические бизнес-данные и обеспечивающие основной функционал. Однако тестирование баз данных часто получает меньше внимания, чем тестирование приложений, что приводит к проблемам в продакшене, от повреждения данных до катастрофической деградации производительности. Это всеобъемлющее руководство охватывает ключевые аспекты тестирования баз данных—от обеспечения целостности данных до валидации сложных миграций—снабжая QA-инженеров практическими стратегиями для тщательной валидации баз данных.

Понимание Database Testing

Database testing валидирует слой данных приложений, фокусируясь на:

  • Целостность и консистентность данных
  • Корректность схемы
  • Поведение транзакций (ACID-свойства)
  • Производительность под нагрузкой
  • Безопасность и контроль доступа
  • Механизмы backup и восстановления
  • Процессы миграции и обновления

Тестирование целостности и консистентности данных

Целостность данных обеспечивает, что данные остаются точными, полными и консистентными на протяжении их жизненного цикла.

Типы целостности данных

1. Entity Integrity (Целостность сущности)

Entity integrity требует, чтобы каждая таблица имела первичный ключ и чтобы первичный ключ был уникальным и не null.

-- Тест 1: Проверить, что primary key существует
SELECT
  table_name,
  COUNT(*) as pk_count
FROM information_schema.table_constraints
WHERE constraint_type = 'PRIMARY KEY'
  AND table_schema = 'your_database'
GROUP BY table_name
HAVING pk_count = 0;

-- Ожидается: Нет результатов (каждая таблица должна иметь PK)

-- Тест 2: Проверить отсутствие NULL первичных ключей
SELECT * FROM users WHERE id IS NULL;
-- Ожидается: 0 строк

-- Тест 3: Проверить отсутствие дублированных первичных ключей
SELECT id, COUNT(*) as count
FROM users
GROUP BY id
HAVING count > 1;
-- Ожидается: 0 строк

2. Referential Integrity (Ссылочная целостность)

Referential integrity обеспечивает, что отношения между таблицами остаются консистентными.

-- Тест: Найти orphaned записи заказов
SELECT o.*
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE u.id IS NULL;

-- Ожидается: 0 строк (все заказы должны иметь валидных пользователей)

-- Тестирование CASCADE-поведения
BEGIN;

INSERT INTO users (id, email) VALUES (999, 'test@example.com');
INSERT INTO orders (id, user_id, total) VALUES (9999, 999, 100.00);

-- Тест CASCADE DELETE
DELETE FROM users WHERE id = 999;

-- Проверить, что заказ также был удалён
SELECT * FROM orders WHERE id = 9999;
-- Ожидается: 0 строк

ROLLBACK;

3. Domain Integrity (Доменная целостность)

Domain integrity обеспечивает, что значения колонок придерживаются определённых constraints.

-- Тест: Проверить NOT NULL constraints
BEGIN;

INSERT INTO users (id, email, created_at)
VALUES (1, NULL, NOW());

-- Ожидается: Ошибка (если email является NOT NULL)

ROLLBACK;

-- Тест: Проверить CHECK constraints
BEGIN;

INSERT INTO products (id, name, price)
VALUES (1, 'Test Product', -10.00);

-- Ожидается: Ошибка (если существует CHECK constraint: price >= 0)

ROLLBACK;

-- Найти записи с невалидными паттернами данных
SELECT * FROM users WHERE email NOT LIKE '%@%';
SELECT * FROM orders WHERE total < 0;

4. User-Defined Integrity (Пользовательская целостность)

User-defined integrity применяет бизнес-правила, специфичные для приложения.

-- Тест: Итоги заказов соответствуют сумме строк заказа
SELECT
  o.id AS order_id,
  o.total AS order_total,
  SUM(oi.quantity * oi.unit_price) AS calculated_total,
  ABS(o.total - SUM(oi.quantity * oi.unit_price)) AS difference
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
GROUP BY o.id, o.total
HAVING ABS(o.total - SUM(oi.quantity * oi.unit_price)) > 0.01;

-- Ожидается: 0 строк (в пределах толерантности округления)

-- Тест: Балансы счетов корректны
SELECT
  a.id,
  a.balance AS recorded_balance,
  COALESCE(SUM(t.amount), 0) AS calculated_balance,
  a.balance - COALESCE(SUM(t.amount), 0) AS difference
FROM accounts a
LEFT JOIN transactions t ON a.id = t.account_id
GROUP BY a.id, a.balance
HAVING ABS(a.balance - COALESCE(SUM(t.amount), 0)) > 0.01;

-- Ожидается: 0 строк

Тестирование консистентности в распределенных системах

Тестирование eventual consistency

import time

def test_eventual_consistency(db_writer, db_reader):
    # Записать в master
    user_id = db_writer.create_user(email="test@example.com")

    # Retry с exponential backoff
    for attempt in range(5):
        user = db_reader.get_user(user_id)
        if user is None:
            time.sleep(2 ** attempt)
        else:
            break

    # После retry должно быть консистентно
    assert user is not None
    assert user.email == "test@example.com"

Performance Tuning и тестирование

Идентификация медленных запросов

PostgreSQL:

-- Просмотр медленных запросов из pg_stat_statements
SELECT
  query,
  calls,
  total_exec_time,
  mean_exec_time,
  max_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;

-- Идентифицировать запросы с высокой вариацией
SELECT
  query,
  calls,
  mean_exec_time,
  stddev_exec_time,
  (stddev_exec_time / mean_exec_time) AS coefficient_of_variation
FROM pg_stat_statements
WHERE calls > 100
ORDER BY (stddev_exec_time / mean_exec_time) DESC;

MySQL:

-- Включить slow query log
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;

-- Просмотр сводки медленных запросов
SELECT
  query,
  exec_count,
  avg_latency,
  max_latency
FROM sys.statement_analysis
ORDER BY avg_latency DESC
LIMIT 20;

Оптимизация индексов

Идентификация отсутствующих индексов:

-- PostgreSQL: Найти sequential scans на больших таблицах
SELECT
  schemaname,
  tablename,
  seq_scan,
  seq_tup_read,
  idx_scan
FROM pg_stat_user_tables
WHERE seq_scan > 0
  AND seq_tup_read / seq_scan > 10000
ORDER BY seq_tup_read DESC;

Тестирование эффективности индекса:

-- Тест EXPLAIN plan до добавления индекса
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = 'user@example.com';

-- Добавить индекс
CREATE INDEX idx_users_email ON users(email);

-- Тест EXPLAIN plan после добавления индекса
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = 'user@example.com';

Техники оптимизации запросов

**Избегать SELECT ***

-- Плохо: Получает все колонки
SELECT * FROM users WHERE id = 123;

-- Хорошо: Получать только необходимые колонки
SELECT id, email, name FROM users WHERE id = 123;

Избегать N+1 запросов

# Плохо: N+1 query problem
users = db.query("SELECT * FROM users")
for user in users:
    orders = db.query(f"SELECT * FROM orders WHERE user_id = {user.id}")

# Хорошо: Использовать JOIN
result = db.query("""
    SELECT u.id, u.name, o.id as order_id, o.total
    FROM users u
    LEFT JOIN orders o ON u.id = o.user_id
""")

Migration Testing

Миграции баз данных — одни из самых рискованных операций в разработке ПО.

Стратегия тестирования миграций

1. Тестировать в non-production сначала (всегда!) 2. Использовать инструменты миграции (Flyway, Liquibase, Alembic) 3. Делать миграции обратимыми 4. Тестировать с production-like объёмами данных 5. Измерять длительность миграции

Тестирование schema-миграций

-- Миграция: добавить колонку
-- UP
ALTER TABLE users ADD COLUMN email_verified BOOLEAN DEFAULT FALSE NOT NULL;
CREATE INDEX idx_users_email_verified ON users(email_verified);

-- DOWN (rollback)
DROP INDEX idx_users_email_verified;
ALTER TABLE users DROP COLUMN email_verified;

Чеклист тестирования:

  • ✓ Миграция выполняется без ошибок
  • ✓ Дефолтное значение корректно применено
  • ✓ NOT NULL constraint не ломает существующие данные
  • ✓ Индекс создан успешно
  • ✓ Rollback-скрипт работает корректно

Тестирование миграций данных

-- Миграция: Разделить колонку name на first_name и last_name
ALTER TABLE users ADD COLUMN first_name VARCHAR(100);
ALTER TABLE users ADD COLUMN last_name VARCHAR(100);

UPDATE users
SET
  first_name = SPLIT_PART(name, ' ', 1),
  last_name = SPLIT_PART(name, ' ', 2)
WHERE name IS NOT NULL;

Тестирование масштабных миграций

Batch-обработка:

-- Вместо единичного UPDATE на миллионы строк:
DO $$
DECLARE
  batch_size INT := 10000;
  rows_updated INT;
BEGIN
  LOOP
    UPDATE users
    SET status = 'active'
    WHERE id IN (
      SELECT id FROM users
      WHERE status IS NULL
      LIMIT batch_size
    );

    GET DIAGNOSTICS rows_updated = ROW_COUNT;
    EXIT WHEN rows_updated = 0;

    COMMIT;
    RAISE NOTICE 'Updated % rows', rows_updated;

    PERFORM pg_sleep(0.1);
  END LOOP;
END $$;

Тестирование NoSQL баз данных

Тестирование MongoDB

// Определить правила валидации схемы
db.createCollection("users", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["email", "createdAt"],
      properties: {
        email: {
          bsonType: "string",
          pattern: "^.+@.+$"
        },
        age: {
          bsonType: "int",
          minimum: 0,
          maximum: 150
        }
      }
    }
  }
});

// Тест валидации
db.users.insertOne({
  email: "user@example.com",
  age: 25,
  createdAt: new Date()
});

Тестирование Cassandra

from cassandra.cluster import Cluster
from cassandra import ConsistencyLevel

def test_consistency_levels():
    # Писать с QUORUM
    insert_stmt = session.prepare("""
        INSERT INTO users (id, email, name)
        VALUES (?, ?, ?)
    """)
    insert_stmt.consistency_level = ConsistencyLevel.QUORUM

    session.execute(insert_stmt, (uuid.uuid4(), 'test@example.com', 'Test'))

Тестирование Redis

import redis
import time

def test_cache_expiration():
    r = redis.Redis(host='localhost', port=6379)

    # Установить значение с TTL
    r.setex('user:123:profile', 10, '{"name": "John"}')

    # Немедленно доступно
    assert r.get('user:123:profile') is not None

    # Истекло после 11 секунд
    time.sleep(11)
    assert r.get('user:123:profile') is None

Заключение

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

Ключевые выводы:

  1. Целостность данных первостепенна - Использовать constraints, валидировать relationships
  2. Производительность масштабируется экспоненциально - Оптимизировать рано, мониторить непрерывно
  3. Миграции высокорискованны - Тестировать тщательно, делать обратимыми
  4. NoSQL (как обсуждается в SQL Injection and XSS: Finding Vulnerabilities) требует других стратегий - Понимать consistency models
  5. Автоматизировать database-тесты (как обсуждается в Database Performance Testing: Query Optimization) - Интеграция в CI/CD ловит регрессии рано
  6. Мониторить production databases (как обсуждается в Stress Testing vs Volume Testing: Key Differences) - Использовать slow query logs, отслеживать метрики

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