Зачем тестировать базы данных напрямую?

API-тесты проверяют, что возвращает API, но не проверяют, что реально хранится в базе данных. API может вернуть успешный ответ, тогда как записанные данные некорректны, неполны или нарушают ограничения.

Тестирование баз данных выявляет проблемы, которые пропускают API-тесты:

  • Данные записаны в неправильную таблицу или колонку
  • Отсутствующее или некорректное применение ограничений
  • Побочные эффекты триггеров, не отражённые в ответах API
  • Проблемы производительности индексов на реальных объёмах данных
  • Нарушения изоляции транзакций, вызывающие повреждение данных

Основы SQL для тестировщиков

Проверка данных после API-вызовов

После вызова endpoint API запросите базу данных для подтверждения операции:

-- После POST /api/users с name="Alice", email="alice@test.com"
SELECT id, name, email, created_at
FROM users
WHERE email = 'alice@test.com';

Тестирование ограничений

Ограничение NOT NULL:

INSERT INTO users (name, email) VALUES (NULL, 'test@test.com');
-- Ожидается: ERROR: null value in column "name" violates not-null constraint

Ограничение UNIQUE:

INSERT INTO users (name, email) VALUES ('Alice', 'alice@test.com');
INSERT INTO users (name, email) VALUES ('Bob', 'alice@test.com');
-- Ожидается: ERROR: duplicate key value violates unique constraint

Ограничение FOREIGN KEY:

INSERT INTO orders (user_id, total) VALUES (99999, 50.00);
-- Ожидается: ERROR: insert or update violates foreign key constraint

Ограничение CHECK:

INSERT INTO products (name, price) VALUES ('Widget', -10.00);
-- Ожидается: ERROR: new row violates check constraint

Тестирование типов данных и точности

-- Денежные значения должны использовать DECIMAL, не FLOAT
SELECT CAST(0.1 + 0.2 AS FLOAT);         -- Может вернуть 0.30000000000000004
SELECT CAST(0.1 + 0.2 AS DECIMAL(10,2)); -- Возвращает 0.30

Тестирование хранимых процедур

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

CALL transfer_funds(
  @from_account := 'ACC001',
  @to_account := 'ACC002',
  @amount := 100.00
);

-- Проверяем: исходный счёт уменьшился на 100
SELECT balance FROM accounts WHERE account_id = 'ACC001';

-- Проверяем: целевой счёт увеличился на 100
SELECT balance FROM accounts WHERE account_id = 'ACC002';

Пограничные случаи:

  • Перевод больше доступного баланса (должен завершиться ошибкой)
  • Перевод на тот же счёт (должен завершиться ошибкой или быть no-op)
  • Перевод нулевой или отрицательной суммы (должен завершиться ошибкой)
  • Конкурентные переводы, превышающие баланс

Тестирование транзакций и изоляции

Свойства ACID

Тест атомарности: Начните транзакцию с несколькими операторами. Вызовите сбой на полпути. Проверьте, что ни один оператор не применился.

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 99999;
ROLLBACK;

SELECT balance FROM accounts WHERE id = 1;
-- Должен быть исходный баланс

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

Индексы ускоряют запросы, но имеют компромиссы. Проверьте:

  1. Запросы по индексированным колонкам используют индекс (проверка через EXPLAIN).
  2. Индексы корректно работают с NULL-значениями.
  3. Составные индексы работают для ожидаемых паттернов запросов.
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'alice@test.com';
-- Ищите "Index Scan" или "Index Only Scan" (не "Seq Scan")

Управление тестовыми данными

Стратегии подготовки

1. Откат транзакции (быстрее всего):

BEGIN → Вставка данных → Тесты → Проверка → ROLLBACK

2. Truncate после теста:

TRUNCATE TABLE orders CASCADE;
TRUNCATE TABLE users CASCADE;

3. Выделенная тестовая база: Создавайте свежую базу для каждого прогона.

Упражнение: Лаборатория тестирования SQL-базы данных

Подготовка

Используйте PostgreSQL с Docker:

docker run -d --name postgres-test \
  -e POSTGRES_PASSWORD=testpass \
  -e POSTGRES_DB=testdb \
  -p 5432:5432 \
  postgres:16

Создайте тестовую схему:

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    role VARCHAR(20) DEFAULT 'user' CHECK (role IN ('user', 'admin', 'moderator')),
    balance DECIMAL(10, 2) DEFAULT 0.00 CHECK (balance >= 0),
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id) ON DELETE RESTRICT,
    total DECIMAL(10, 2) NOT NULL CHECK (total > 0),
    status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled')),
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE order_items (
    id SERIAL PRIMARY KEY,
    order_id INTEGER REFERENCES orders(id) ON DELETE CASCADE,
    product_name VARCHAR(200) NOT NULL,
    quantity INTEGER NOT NULL CHECK (quantity > 0),
    unit_price DECIMAL(10, 2) NOT NULL CHECK (unit_price >= 0)
);

Задание 1: Тестирование ограничений

Напишите SQL-операторы, тестирующие каждое ограничение. Задокументируйте ожидаемую ошибку:

  1. Вставить пользователя с NULL-именем.
  2. Вставить двух пользователей с одинаковым email.
  3. Вставить пользователя с ролью ‘superadmin’.
  4. Вставить пользователя с отрицательным балансом.
  5. Вставить заказ для несуществующего user_id.
  6. Вставить заказ с total = 0.
  7. Вставить order_item с quantity = 0.
  8. Удалить пользователя с заказами (RESTRICT должен предотвратить).
  9. Удалить заказ (CASCADE должен удалить его order_items).

Задание 2: Проверка целостности данных

Вставьте тестовые данные и проверьте целостность запросами.

Задание 3: Тестирование транзакций

Проверьте атомарность операции «создание заказа»:

  1. Начните транзакцию → вставьте заказ → вставьте позиции → спишите баланс → commit.
  2. Повторите с недостаточным балансом → проверьте полный rollback.

Задание 4: Тестирование производительности

  1. Вставьте 100,000 пользователей со случайными данными.
  2. Запросите по email — замерьте время через EXPLAIN ANALYZE.
  3. Удалите индекс и повторите — сравните время.

Результаты

  1. SQL-скрипты для всех тестов ограничений с результатами.
  2. Запросы проверки целостности с результатами.
  3. Скрипты тестов транзакций.
  4. Сравнение производительности с/без индекса.