Манипуляция данными
Updated Jul 2026
Настройка тестовых данных напрямую в базе данных быстрее, чем через UI или API. INSERT, UPDATE и DELETE позволяют создавать точные тестовые состояния, манипулировать данными для тестирования граничных случаев и очищать данные после прогонов тестов. Но с большой силой приходит большая ответственность — небезопасная манипуляция данными в тестовых окружениях может повредить общие ресурсы.
INSERT: Создание тестовых данных
-- Create a test user with specific attributes
INSERT INTO users (id, name, email, role, created_at)
VALUES ('test-uuid-001', 'Test User', 'testuser@automation.com', 'admin',
NOW() - INTERVAL '90 days');
-- Create multiple orders for the test user
INSERT INTO orders (id, user_id, status, total, created_at) VALUES
('order-001', 'test-uuid-001', 'completed', 99.99, NOW() - INTERVAL '30 days'),
('order-002', 'test-uuid-001', 'pending', 149.50, NOW() - INTERVAL '1 day'),
('order-003', 'test-uuid-001', 'cancelled', 75.00, NOW() - INTERVAL '15 days');
-- Create line items for an order
INSERT INTO line_items (order_id, product_id, quantity, unit_price) VALUES
('order-001', 'prod-a', 2, 25.00),
('order-001', 'prod-b', 1, 49.99);
Почему прямая настройка через БД быстрее
| Метод | Время на создание пользователя + 3 заказа | Зависимости |
|---|---|---|
| Клики в UI | Минуты | Браузер, загрузка страниц, формы |
| API-вызовы | Секунды | Поток авторизации, бизнес-логика |
| Прямой SQL | Миллисекунды | Только подключение к БД |
Для сложных тестовых сценариев (пользователь со 100 заказами, специфическое распределение дат, граничные данные) SQL на порядки быстрее.
UPDATE: Модификация тестового состояния
-- Set a user's password to expired (test expired password flow)
UPDATE users
SET password_expires_at = NOW() - INTERVAL '1 day'
WHERE email = 'testuser@automation.com';
-- Set an order to a specific status (test status-dependent logic)
UPDATE orders
SET status = 'shipped', shipped_at = NOW() - INTERVAL '2 hours'
WHERE id = 'order-001';
-- Simulate a user who has been inactive for 6 months
UPDATE users
SET last_login_at = NOW() - INTERVAL '6 months'
WHERE email = 'testuser@automation.com';
DELETE: Очистка
-- Cleanup in reverse dependency order (child records first)
DELETE FROM line_items WHERE order_id IN (
SELECT id FROM orders WHERE user_id = 'test-uuid-001'
);
DELETE FROM orders WHERE user_id = 'test-uuid-001';
DELETE FROM users WHERE id = 'test-uuid-001';
Почему важен порядок зависимостей
Ограничения внешних ключей предотвращают удаление родительской записи при наличии дочерних. Если вы попытаетесь удалить пользователя, у которого есть заказы, база данных отклонит удаление. Всегда удаляйте в обратном порядке зависимостей: сначала line_items, затем orders, затем users.
Транзакции: Страховочная сетка
Транзакции обеспечивают, что изменения тестовых данных атомарны (всё или ничего) и могут быть откачены.
Транзакция для настройки тестов
-- Everything succeeds or nothing does
BEGIN;
INSERT INTO users (id, name, email, role)
VALUES ('test-uuid-002', 'Transaction User', 'txn@test.com', 'viewer');
INSERT INTO orders (id, user_id, status, total)
VALUES ('order-txn', 'test-uuid-002', 'pending', 50.00);
-- If either INSERT fails, ROLLBACK undoes everything
COMMIT;
Изоляция тестов через транзакции
Наиболее мощный паттерн для управления тестовыми данными: оборачивайте каждый тест в транзакцию и откатывайте её после. База данных возвращается к исходному состоянию после каждого теста.
@pytest.fixture
def db_transaction(db):
"""Wrap each test in a transaction that rolls back."""
db.autocommit = False
yield db
db.rollback() # All changes are undone after each test
def test_user_creation(db_transaction, api_client):
api_client.post("/users", json={"name": "Alice", "email": "alice@test.com"})
cursor = db_transaction.cursor()
cursor.execute("SELECT * FROM users WHERE email = 'alice@test.com'")
assert cursor.fetchone() is not None
# After the test, db.rollback() removes the user automatically
def test_another_test(db_transaction):
# This test starts with a clean database — no Alice from the previous test
cursor = db_transaction.cursor()
cursor.execute("SELECT * FROM users WHERE email = 'alice@test.com'")
assert cursor.fetchone() is None # Alice does not exist
Ограничения транзакций
| Сценарий | Откат транзакции работает? |
|---|---|
| Тест создаёт данные через прямой SQL | Да |
| Тест создаёт данные через API-вызов к той же БД | Да (если API использует то же соединение) |
| Тест создаёт данные через API-вызов к отдельному сервису | Нет (другое соединение/транзакция) |
| Тест модифицирует внешние системы (S3, email) | Нет (нельзя откатить внешние побочные эффекты) |
Для API-тестов, где откат невозможен, используйте фикстуры очистки:
@pytest.fixture
def create_user(api_client):
created_ids = []
def _create(**kwargs):
r = api_client.post("/users", json=kwargs)
user = r.json()
created_ids.append(user["id"])
return user
yield _create
for uid in created_ids:
api_client.delete(f"/users/{uid}")
Безопасные практики
| Практика | Зачем |
|---|---|
| Используйте транзакции с откатом | BEGIN; ... ROLLBACK; — тестовые данные никогда не сохраняются |
| Используйте идентифицируемые префиксы | test-*, automation-* — легко найти и очистить |
| Никогда не выполняйте деструктивный SQL в продакшене | Используйте read-only учётные данные для верификации |
| Очищайте в обратном порядке зависимостей | Избегайте нарушений ограничений внешних ключей |
| Используйте уникальные идентификаторы | Предотвращайте коллизии между параллельными прогонами тестов |
Идентифицируемые тестовые данные
-- Prefix all test data for easy identification and cleanup
INSERT INTO users (id, name, email) VALUES
('test-auto-001', 'AUTO Test User 1', 'auto-test-1@automation.test'),
('test-auto-002', 'AUTO Test User 2', 'auto-test-2@automation.test');
-- Emergency cleanup: find and remove all automation test data
DELETE FROM orders WHERE user_id LIKE 'test-auto-%';
DELETE FROM users WHERE id LIKE 'test-auto-%';
-- OR
DELETE FROM users WHERE email LIKE '%@automation.test';
Read-only доступ к продакшену
@pytest.fixture(scope="session")
def prod_db():
"""Read-only connection to production for verification queries only."""
conn = psycopg2.connect(
host=os.environ["PROD_DB_HOST"],
dbname="production",
user="readonly_user", # Read-only database user
password=os.environ["PROD_DB_READONLY_PASS"],
options="-c default_transaction_read_only=on" # Extra safety
)
yield conn
conn.close()
Массовая генерация данных
Для тестирования производительности или сценариев с большими наборами данных:
-- Generate 1000 test users
INSERT INTO users (id, name, email, role, created_at)
SELECT
'perf-test-' || generate_series AS id,
'Perf User ' || generate_series AS name,
'perftest' || generate_series || '@test.com' AS email,
CASE WHEN generate_series % 3 = 0 THEN 'admin'
WHEN generate_series % 3 = 1 THEN 'editor'
ELSE 'viewer' END AS role,
NOW() - (generate_series || ' days')::INTERVAL AS created_at
FROM generate_series(1, 1000);
Практическое упражнение
- Напишите INSERT-запросы для создания пользователя с 3 заказами и 2 позициями в каждом заказе
- Напишите скрипт очистки, который удаляет все тестовые данные в правильном порядке зависимостей
- Создайте pytest-фикстуру, которая оборачивает тесты в транзакции с откатом
- Напишите тест, который создаёт данные через API и проверяет их в базе данных, затем убедитесь, что откат их очистил
- Напишите скрипт массовой генерации данных, который создаёт 500 пользователей с случайными ролями
Ключевые выводы
- Прямой SQL — самый быстрый способ создания тестовых данных
- Всегда очищайте в обратном порядке зависимостей (дочерние перед родительскими)
- Откат транзакций — чистейший паттерн изоляции тестов
- Используйте идентифицируемые префиксы для тестовых данных (
test-*,auto-*) - Никогда не выполняйте операции записи в продакшене — используйте read-only учётные данные
- Для данных, созданных через API, которые нельзя откатить, используйте фикстуры очистки