Modern QA2026Манипуляция данными
Join

Course15 SQL & Database Testing

Foundations · Chapter 15

Манипуляция данными

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);

Практическое упражнение

  1. Напишите INSERT-запросы для создания пользователя с 3 заказами и 2 позициями в каждом заказе
  2. Напишите скрипт очистки, который удаляет все тестовые данные в правильном порядке зависимостей
  3. Создайте pytest-фикстуру, которая оборачивает тесты в транзакции с откатом
  4. Напишите тест, который создаёт данные через API и проверяет их в базе данных, затем убедитесь, что откат их очистил
  5. Напишите скрипт массовой генерации данных, который создаёт 500 пользователей с случайными ролями

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

  • Прямой SQL — самый быстрый способ создания тестовых данных
  • Всегда очищайте в обратном порядке зависимостей (дочерние перед родительскими)
  • Откат транзакций — чистейший паттерн изоляции тестов
  • Используйте идентифицируемые префиксы для тестовых данных (test-*, auto-*)
  • Никогда не выполняйте операции записи в продакшене — используйте read-only учётные данные
  • Для данных, созданных через API, которые нельзя откатить, используйте фикстуры очистки