Зачем тестировать базы данных напрямую?
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;
-- Должен быть исходный баланс
Тестирование индексов
Индексы ускоряют запросы, но имеют компромиссы. Проверьте:
- Запросы по индексированным колонкам используют индекс (проверка через EXPLAIN).
- Индексы корректно работают с NULL-значениями.
- Составные индексы работают для ожидаемых паттернов запросов.
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-операторы, тестирующие каждое ограничение. Задокументируйте ожидаемую ошибку:
- Вставить пользователя с NULL-именем.
- Вставить двух пользователей с одинаковым email.
- Вставить пользователя с ролью ‘superadmin’.
- Вставить пользователя с отрицательным балансом.
- Вставить заказ для несуществующего user_id.
- Вставить заказ с total = 0.
- Вставить order_item с quantity = 0.
- Удалить пользователя с заказами (RESTRICT должен предотвратить).
- Удалить заказ (CASCADE должен удалить его order_items).
Задание 2: Проверка целостности данных
Вставьте тестовые данные и проверьте целостность запросами.
Задание 3: Тестирование транзакций
Проверьте атомарность операции «создание заказа»:
- Начните транзакцию → вставьте заказ → вставьте позиции → спишите баланс → commit.
- Повторите с недостаточным балансом → проверьте полный rollback.
Задание 4: Тестирование производительности
- Вставьте 100,000 пользователей со случайными данными.
- Запросите по email — замерьте время через EXPLAIN ANALYZE.
- Удалите индекс и повторите — сравните время.
Результаты
- SQL-скрипты для всех тестов ограничений с результатами.
- Запросы проверки целостности с результатами.
- Скрипты тестов транзакций.
- Сравнение производительности с/без индекса.