SELECT-запросы для верификации
Updated Jul 2026
После выполнения действия через UI или API проверьте результат на уровне базы данных. База данных — источник истины: если UI говорит «заказ создан», но в базе нет записи, данные на самом деле не были сохранены.
Базовые верификационные запросы
-- Verify user was stored correctly after API creation
SELECT id, name, email, role, created_at
FROM users
WHERE email = 'newuser@test.com';
-- Verify soft-delete worked (deleted_at should be set, not NULL)
SELECT id, email, deleted_at
FROM users
WHERE email = 'removed@test.com'
AND deleted_at IS NOT NULL;
-- Verify order total matches line items (returns rows only if inconsistent)
SELECT o.id, o.total, SUM(li.quantity * li.unit_price) AS calculated_total
FROM orders o
JOIN line_items li ON li.order_id = o.id
WHERE o.id = 42
GROUP BY o.id, o.total
HAVING o.total != SUM(li.quantity * li.unit_price);
Третий запрос — паттерн, который стоит запомнить: он возвращает строки только при наличии расхождения. Отсутствие возвращённых строк означает, что данные согласованы.
Использование SQL в автоматизации тестов
Подключение к базе данных из тестового кода позволяет напрямую проверять сохранение данных:
import psycopg2
import pytest
@pytest.fixture(scope="session")
def db():
conn = psycopg2.connect(
host="localhost",
dbname="testdb",
user="testuser",
password="testpass"
)
yield conn
conn.close()
def test_user_creation_persists(api_client, db):
"""Verify that creating a user via API persists to database."""
api_client.post("/users", json={
"name": "Alice",
"email": "alice@test.com"
})
cursor = db.cursor()
cursor.execute(
"SELECT name, email, role FROM users WHERE email = %s",
("alice@test.com",)
)
row = cursor.fetchone()
assert row is not None, "User was not persisted to database"
assert row[0] == "Alice"
assert row[2] == "viewer" # Verify default role applied
Почему верификация через БД важна
| Сценарий | Ответ API | Реальность в БД | Без проверки БД |
|---|---|---|---|
| Баг кеширования | Возвращает 201 Created | Нет строки в таблице users | Баг не обнаружен |
| Состояние гонки | Возвращает успех | Созданы дубликаты строк | Повреждение данных не обнаружено |
| Баг значения по умолчанию | Возвращает пользователя с role=null | Столбец role равен NULL | Отсутствующее значение по умолчанию не обнаружено |
| Баг мягкого удаления | Возвращает 204 Deleted | deleted_at всё ещё NULL | Элемент выглядит удалённым, но таковым не является |
Типичные паттерны запросов для QA
Проверка существования записи
-- Does this user exist?
SELECT COUNT(*) FROM users WHERE email = 'test@example.com';
-- Expected: 1
-- Does this order have line items?
SELECT COUNT(*) FROM line_items WHERE order_id = 'order-123';
-- Expected: > 0
Проверка значений по умолчанию
-- Check that defaults are applied on creation
SELECT role, active, email_verified, created_at, updated_at
FROM users
WHERE email = 'newuser@test.com';
-- Expected: role='viewer', active=true, email_verified=false,
-- created_at is recent, updated_at equals created_at
Проверка временных меток
-- Check that updated_at changes on modification
SELECT updated_at > created_at AS was_modified
FROM users
WHERE id = 123;
-- Expected: true (if user was updated)
-- Check that timestamps are reasonable (not in the future, not from 1970)
SELECT *
FROM orders
WHERE created_at > NOW()
OR created_at < '2020-01-01';
-- Expected: no rows (no invalid timestamps)
Поиск аномалий данных
-- Find orphaned records (line items without orders)
SELECT li.id, li.order_id
FROM line_items li
LEFT JOIN orders o ON o.id = li.order_id
WHERE o.id IS NULL;
-- Find duplicate emails (data integrity issue)
SELECT email, COUNT(*)
FROM users
GROUP BY email
HAVING COUNT(*) > 1;
-- Find NULL values in required fields
SELECT id, name, email
FROM users
WHERE name IS NULL OR email IS NULL;
Параметризованные запросы (предотвращение SQL-инъекций)
Всегда используйте параметризованные запросы в тестовом коде:
# BAD: string interpolation — SQL injection vulnerability
cursor.execute(f"SELECT * FROM users WHERE email = '{email}'")
# GOOD: parameterized query — safe
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
# GOOD: named parameters
cursor.execute(
"SELECT * FROM users WHERE email = %(email)s AND role = %(role)s",
{"email": email, "role": "admin"}
)
Даже в тестовом коде используйте параметризованные запросы. Это хорошая привычка, предотвращает случайную инъекцию, когда тестовые данные содержат специальные символы, и радует код-ревьюеров.
Обработка результатов запросов в Python
# Using DictCursor for named access
from psycopg2.extras import DictCursor
def test_user_fields(db):
cursor = db.cursor(cursor_factory=DictCursor)
cursor.execute("SELECT * FROM users WHERE email = %s", ("alice@test.com",))
user = cursor.fetchone()
assert user["name"] == "Alice"
assert user["role"] == "viewer"
assert user["created_at"] is not None
# Fetching multiple rows
def test_all_active_users_have_email(db):
cursor = db.cursor(cursor_factory=DictCursor)
cursor.execute("SELECT id, email FROM users WHERE active = true")
users = cursor.fetchall()
for user in users:
assert user["email"] is not None, f"User {user['id']} has no email"
assert "@" in user["email"], f"User {user['id']} has invalid email: {user['email']}"
Практическое упражнение
Напишите SQL-запросы верификации для следующих сценариев:
- После создания пользователя через API проверьте, что пользователь существует в БД с корректными именем, email и ролью по умолчанию
- После создания заказа проверьте, что сумма заказа совпадает с суммой позиций
- После мягкого удаления пользователя проверьте, что временная метка deleted_at установлена и пользователь всё ещё существует в таблице
- Найдите всех пользователей, зарегистрированных за последние 24 часа (для пост-деплойной проверки)
- Найдите заказы с отрицательными итогами (не должны существовать)
- Интегрируйте один из этих запросов в pytest-тест с использованием psycopg2
Ключевые выводы
- База данных — источник истины: проверяйте критическое сохранение данных с помощью SQL
- Используйте SQL в автоматизации тестов для дополнения утверждений API
- Типичные паттерны: проверка существования, верификация значений по умолчанию, валидация временных меток, обнаружение аномалий
- Всегда используйте параметризованные запросы, даже в тестовом коде
- DictCursor обеспечивает доступ к результатам запроса по имени для читаемых тестовых утверждений