Modern QA2026Паттерны Async/Await
Join

Course12 Programming for QA

Foundations · Chapter 12

Паттерны Async/Await

Updated Jul 2026

Современные тестовые фреймворки активно используют асинхронное выполнение. Автоматизация браузера (Playwright, Puppeteer), конкурентное тестирование API и событийно-ориентированные архитектуры — всё это требует понимания async/await. Непонимание асинхронных паттернов — наиболее частый источник нестабильных тестов и загадочных зависаний в современных тест-сьютах.

Почему асинхронность важна для QA

Когда ваш тест нажимает кнопку, браузер не обновляется мгновенно. Когда тест отправляет API-запрос, ответ не приходит мгновенно. Асинхронное программирование предоставляет способ писать код, который ожидает завершения этих операций, не блокируя весь запуск тестов.

Синхронное Асинхронное
Каждая операция блокирует до завершения Операции могут выполняться параллельно
Просто для понимания Требует понимания цикла событий
Медленно для задач с интенсивным I/O Быстро для параллельных операций ввода/вывода
requests.get() await httpx.get()

Асинхронные паттерны TypeScript/JavaScript

Базовый Async/Await

// Every Playwright interaction is async
test('user can add item to cart', async ({ page }) => {
    await page.goto('/products');
    await page.click('[data-testid="add-to-cart"]');
    await expect(page.locator('.cart-count')).toHaveText('1');
});

Ключевое слово await приостанавливает выполнение до завершения операции. Без него код продолжится немедленно — часто до того, как операция завершится.

Ошибка забытого await

// BUG: missing await — test passes incorrectly
test('should have correct title', async ({ page }) => {
    await page.goto('/dashboard');
    // This returns a Promise, which is truthy — so the assert passes regardless!
    expect(page.title()).toBe('Dashboard');
    // FIX: await the title() call
    expect(await page.title()).toBe('Dashboard');
});

Это самая распространённая асинхронная ошибка в автоматизации тестирования. Тест проверяет объект Promise (который является truthy), а не фактическое значение. Тест проходит, даже если заголовок неправильный.

Параллельные API-запросы

// Sequential: each request waits for the previous one (slow)
const user = await fetch('/api/users/1');
const orders = await fetch('/api/orders?user=1');
const payments = await fetch('/api/payments?user=1');
// Total time: user + orders + payments

// Concurrent: all requests run in parallel (fast)
const [user, orders, payments] = await Promise.all([
    fetch('/api/users/1'),
    fetch('/api/orders?user=1'),
    fetch('/api/payments?user=1'),
]);
// Total time: max(user, orders, payments)

Promise.allSettled для устойчивых тестов

// Promise.all fails fast — if one request fails, all fail
// Promise.allSettled waits for all to complete, regardless of success/failure
const results = await Promise.allSettled([
    fetch('/api/endpoint-a'),
    fetch('/api/endpoint-b'),
    fetch('/api/endpoint-c'),
]);

const failures = results.filter(r => r.status === 'rejected');
const successes = results.filter(r => r.status === 'fulfilled');

expect(failures).toHaveLength(0);

Асинхронные паттерны Python

Основы asyncio

import asyncio
import httpx

async def test_concurrent_requests():
    async with httpx.AsyncClient() as client:
        tasks = [client.get(f"https://api.example.com/items/{i}") for i in range(100)]
        responses = await asyncio.gather(*tasks)
        assert all(r.status_code == 200 for r in responses)

pytest-asyncio

import pytest
import httpx

@pytest.mark.asyncio
async def test_api_health(base_url):
    async with httpx.AsyncClient() as client:
        response = await client.get(f"{base_url}/health")
        assert response.status_code == 200

@pytest.mark.asyncio
async def test_parallel_endpoint_availability(base_url):
    endpoints = ["/users", "/products", "/orders", "/health"]
    async with httpx.AsyncClient() as client:
        tasks = [client.get(f"{base_url}{ep}") for ep in endpoints]
        responses = await asyncio.gather(*tasks)

    for endpoint, response in zip(endpoints, responses):
        assert response.status_code == 200, f"{endpoint} returned {response.status_code}"

asyncio.wait_for с таймаутом

async def test_slow_endpoint_timeout():
    async with httpx.AsyncClient() as client:
        try:
            response = await asyncio.wait_for(
                client.get("https://api.example.com/slow-endpoint"),
                timeout=5.0
            )
            assert response.status_code == 200
        except asyncio.TimeoutError:
            pytest.fail("Endpoint did not respond within 5 seconds")

Типичные асинхронные ловушки

1. Забытый await

Уже рассмотрено выше — самая опасная ошибка, потому что тест проходит без предупреждений.

Обнаружение: используйте строгий режим TypeScript и правила ESLint, такие как @typescript-eslint/no-floating-promises.

2. Состояния гонки в тестах

// BUG: click may happen before navigation completes
await page.goto('/products');
page.click('[data-testid="first-product"]');  // missing await!
await expect(page).toHaveURL(/\/products\/\d+/);  // may fail intermittently

// FIX: await the click
await page.goto('/products');
await page.click('[data-testid="first-product"]');
await expect(page).toHaveURL(/\/products\/\d+/);

3. Общее состояние между параллельными тестами

# BUG: parallel tests modify the same user
async def test_update_name():
    await api.patch("/users/1", json={"name": "Alice"})
    user = await api.get("/users/1")
    assert user["name"] == "Alice"  # May fail if test_update_email runs concurrently

async def test_update_email():
    await api.patch("/users/1", json={"email": "new@test.com"})
    user = await api.get("/users/1")
    assert user["email"] == "new@test.com"

# FIX: each test creates its own user
async def test_update_name():
    user = await api.post("/users", json={"name": "Original"})
    await api.patch(f"/users/{user['id']}", json={"name": "Alice"})
    updated = await api.get(f"/users/{user['id']}")
    assert updated["name"] == "Alice"

4. Необработанные отклонения промисов

// BUG: if the fetch fails, the error is swallowed
fetch('/api/cleanup').then(r => r.json());  // no await, no catch

// FIX: always await or catch
try {
    await fetch('/api/cleanup');
} catch (e) {
    console.warn('Cleanup failed:', e);
}

5. Блокировка цикла событий

# BUG: synchronous sleep blocks the event loop
async def test_with_delay():
    import time
    time.sleep(5)  # Blocks the entire event loop for 5 seconds!

# FIX: use async sleep
async def test_with_delay():
    await asyncio.sleep(5)  # Non-blocking

Асинхронность в тестовых фреймворках

Фреймворк Поддержка асинхронности Примечания
Playwright Нативная Каждое взаимодействие возвращает Promise
Cypress Абстрагированная Команды автоматически связываются; нет явного await
pytest Через pytest-asyncio Требуется декоратор @pytest.mark.asyncio
Jest Нативная Тестовые функции могут быть async
Selenium Синхронный по умолчанию Python-привязки синхронны; доступна async-обёртка

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

  1. Напишите тест, отправляющий 50 параллельных API-запросов с помощью Promise.all / asyncio.gather, и проверьте, что все возвращают 200
  2. Намеренно создайте ошибку «забытый await» и наблюдайте, как тест проходит некорректно. Затем исправьте.
  3. Напишите тест с таймаутом: если эндпоинт не отвечает в течение 3 секунд, тест должен упасть
  4. Создайте два теста, которые конфликтовали бы при параллельном запуске (общее состояние), затем рефакторьте их для безопасного параллельного выполнения

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

  • Каждая асинхронная операция должна быть ожидаема (await) — забытый await является самой частой асинхронной ошибкой
  • Используйте Promise.all / asyncio.gather для параллельных операций
  • Используйте Promise.allSettled, когда нужны все результаты, независимо от индивидуальных сбоев
  • Избегайте общего состояния между параллельными тестами — каждый тест должен создавать свои данные
  • Никогда не блокируйте цикл событий синхронными операциями в асинхронном коде
  • Включите правила линтера для обнаружения пропущенного await на этапе компиляции