Паттерны 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-обёртка |
Практическое упражнение
- Напишите тест, отправляющий 50 параллельных API-запросов с помощью
Promise.all/asyncio.gather, и проверьте, что все возвращают 200 - Намеренно создайте ошибку «забытый await» и наблюдайте, как тест проходит некорректно. Затем исправьте.
- Напишите тест с таймаутом: если эндпоинт не отвечает в течение 3 секунд, тест должен упасть
- Создайте два теста, которые конфликтовали бы при параллельном запуске (общее состояние), затем рефакторьте их для безопасного параллельного выполнения
Ключевые выводы
- Каждая асинхронная операция должна быть ожидаема (await) — забытый await является самой частой асинхронной ошибкой
- Используйте
Promise.all/asyncio.gatherдля параллельных операций - Используйте
Promise.allSettled, когда нужны все результаты, независимо от индивидуальных сбоев - Избегайте общего состояния между параллельными тестами — каждый тест должен создавать свои данные
- Никогда не блокируйте цикл событий синхронными операциями в асинхронном коде
- Включите правила линтера для обнаружения пропущенного await на этапе компиляции