Modern QA2026Паттерны асинхронного тестирования
Join

Course04 API & Contract Testing with AI

Cutting-edge · Chapter 04

Паттерны асинхронного тестирования

Updated Jul 2026

Матрица паттернов

Асинхронное тестирование требует различных стратегий в зависимости от архитектуры системы. Этот справочник покрывает пять основных паттернов тестирования событийно-ориентированных и асинхронных систем.

Паттерн 1: Опрос и ожидание

Простейший асинхронный паттерн. Проверка ожидаемого состояния с таймаутом.

def wait_for_condition(check_fn, timeout=10, interval=0.5, message="Condition not met"):
    """Poll a condition function until it returns True or timeout."""
    deadline = time.time() + timeout
    last_result = None

    while time.time() < deadline:
        last_result = check_fn()
        if last_result:
            return last_result
        time.sleep(interval)

    pytest.fail(f"{message} (timeout={timeout}s, last_result={last_result})")

# Usage
def test_order_processed(kafka_producer, db):
    kafka_producer.send("order.created", {"order_id": "ord-1"})

    order = wait_for_condition(
        lambda: db.get_order("ord-1"),
        timeout=10,
        message="Order was not processed"
    )
    assert order.status == "pending"

Когда использовать: Простая верификация «событие -> состояние». Одно событие, одно ожидаемое изменение состояния.

Подводные камни:

  • Слишком маленький таймаут вызывает нестабильные тесты
  • Слишком большой интервал пропускает окно (состояние изменяется и изменяется снова)
  • Нет информации о том, почему условие не было выполнено

Улучшенная версия с диагностикой

def wait_for_condition(check_fn, timeout=10, interval=0.5, message="", diagnostics_fn=None):
    """Poll with diagnostics on failure."""
    deadline = time.time() + timeout
    attempts = 0

    while time.time() < deadline:
        attempts += 1
        result = check_fn()
        if result:
            return result
        time.sleep(interval)

    # Gather diagnostics on failure
    diag = ""
    if diagnostics_fn:
        diag = f"\nDiagnostics: {diagnostics_fn()}"

    pytest.fail(
        f"{message}\n"
        f"Timeout: {timeout}s, Attempts: {attempts}, "
        f"Interval: {interval}s{diag}"
    )

Паттерн 2: Перехват колбэка

Поднимите временный HTTP-сервер, зарегистрируйте его как цель вебхука и перехватывайте входящие вызовы.

# See webhook-and-sqs-testing.md for the WebhookCapture implementation

def test_payment_webhook(webhook_server, payment_service):
    """Verify payment completion triggers a webhook."""
    payment_service.register_webhook(
        url=f"{webhook_server.url}/payment-complete",
        events=["payment.completed"]
    )

    payment_service.process_payment(order_id="ord-1", amount=99.99)

    call = webhook_server.wait_for_call(timeout=15)
    assert call is not None, "Payment webhook not received"
    assert call["body"]["order_id"] == "ord-1"
    assert call["body"]["amount"] == 99.99
    assert call["body"]["status"] == "completed"

Когда использовать: Тестирование вебхуков. Верификация того, что внешняя система вызывает вашу конечную точку.

Паттерн 3: Опустошение очереди

Чтение из очереди сообщений до получения всех ожидаемых событий или истечения таймаута.

def drain_queue(queue_url, expected_count, timeout=30):
    """Read messages from a queue until expected count reached or timeout."""
    messages = []
    deadline = time.time() + timeout

    while len(messages) < expected_count and time.time() < deadline:
        batch = poll_sqs(queue_url, timeout=2)
        if batch:
            messages.append(batch)

    return messages

# Usage
def test_order_generates_three_events(api_client, sqs_queues):
    """Creating an order should produce events to billing, shipping, and inventory."""
    api_client.post("/api/orders", json=ORDER_PAYLOAD)

    # Drain all three queues
    billing = drain_queue(sqs_queues["billing"], expected_count=1, timeout=10)
    shipping = drain_queue(sqs_queues["shipping"], expected_count=1, timeout=10)
    inventory = drain_queue(sqs_queues["inventory"], expected_count=1, timeout=10)

    assert len(billing) == 1, "Missing billing event"
    assert len(shipping) == 1, "Missing shipping event"
    assert len(inventory) == 1, "Missing inventory event"

Когда использовать: Событийно-ориентированные системы, где одно действие порождает события в нескольких очередях.

Паттерн 4: Корреляция трассировок

Используйте идентификатор корреляции для трассировки цепочки событий через несколько сервисов.

import uuid

def test_order_flow_end_to_end(api_client, trace_store):
    """Trace an order through the entire event pipeline."""
    correlation_id = str(uuid.uuid4())

    # Initiate the flow
    response = api_client.post("/api/orders", json={
        **ORDER_PAYLOAD,
        "correlation_id": correlation_id,
    })
    assert response.status_code == 201

    # Wait for all services to process
    time.sleep(15)

    # Query the trace store for all events with this correlation ID
    traces = trace_store.get_traces(correlation_id)

    # Verify the expected event chain
    event_types = [t["event_type"] for t in traces]
    assert "order.created" in event_types
    assert "payment.initiated" in event_types
    assert "payment.completed" in event_types
    assert "inventory.reserved" in event_types
    assert "shipping.label_created" in event_types
    assert "notification.sent" in event_types

    # Verify ordering
    order_idx = event_types.index("order.created")
    payment_idx = event_types.index("payment.completed")
    shipping_idx = event_types.index("shipping.label_created")
    assert order_idx < payment_idx < shipping_idx, (
        f"Events out of order: order@{order_idx}, "
        f"payment@{payment_idx}, shipping@{shipping_idx}"
    )

Когда использовать: Сложные потоки, охватывающие несколько сервисов. Верификация того, что вся цепочка завершилась корректно.

Паттерн 5: Сравнение снимков

Сравнение состояния базы данных до и после события для верификации побочных эффектов.

def test_inventory_updated_after_order(db, kafka_producer):
    """Order event should decrement inventory."""
    # Snapshot before
    before = db.get_inventory("prod-789")
    assert before.quantity == 100

    # Send order event
    kafka_producer.send("order.created", {
        "order_id": "ord-snap",
        "items": [{"product_id": "prod-789", "quantity": 3}]
    })

    # Wait for processing
    wait_for_condition(
        lambda: db.get_inventory("prod-789").quantity != 100,
        timeout=10,
        message="Inventory was not updated"
    )

    # Snapshot after
    after = db.get_inventory("prod-789")
    assert after.quantity == 97, (
        f"Expected 97 (100 - 3), got {after.quantity}"
    )

Когда использовать: Верификация состояния. Подтверждение того, что события корректно изменяют персистентное состояние.

Руководство по выбору паттерна

Паттерн Когда использовать Сложность Надёжность
Опрос и ожидание Простая проверка состояния после события Низкая Высокая
Перехват колбэка Верификация вебхуков Средняя Высокая
Опустошение очереди Верификация событий в нескольких очередях Средняя Средняя
Корреляция трассировок Цепочки событий между сервисами Высокая Средняя
Сравнение снимков Верификация состояния базы данных Низкая Высокая

Типичные ошибки асинхронного тестирования

Ошибка 1: Захардкоженный sleep()

# BAD: hardcoded sleep
kafka_producer.send("order.created", event)
time.sleep(5)  # Hope it processed in 5 seconds
order = db.get_order("ord-1")
assert order is not None

# GOOD: poll with timeout
kafka_producer.send("order.created", event)
order = wait_for_condition(
    lambda: db.get_order("ord-1"),
    timeout=10
)
assert order is not None

Ошибка 2: Неочистка очередей между тестами

@pytest.fixture(autouse=True)
def clean_queues(sqs_queues):
    """Drain all queues before each test to prevent cross-contamination."""
    for queue_url in sqs_queues.values():
        drain_queue(queue_url, expected_count=100, timeout=2)
    yield

Ошибка 3: Игнорирование порядка сообщений

При тестировании порядка всегда проверяйте с индексированными утверждениями, а не только проверками наличия:

# BAD: only checks presence
assert "order.created" in events
assert "payment.completed" in events

# GOOD: checks order
assert events.index("order.created") < events.index("payment.completed")

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

Асинхронное тестирование требует явных паттернов для ожидания, перехвата и верификации. Пять паттернов (опрос-и-ожидание, перехват колбэка, опустошение очереди, корреляция трассировок, сравнение снимков) покрывают весь спектр сценариев асинхронного тестирования. Выбирайте в зависимости от архитектуры системы и проверяйте не только факт доставки событий, но и их корректный порядок и содержимое.