← Назад к вопросам
Как работать с асинхронными функциями при тестировании?
2.0 Middle🔥 231 комментариев
#Асинхронность и многопоточность#Тестирование
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Тестирование асинхронных функций
Асинхронный код в Python (asyncio, aiohttp) требует специального подхода к тестированию. Нельзя просто вызвать async функцию как обычную — нужен event loop.
Основные способы тестирования async кода
1. pytest-asyncio для pytest
Самый популярный способ — использовать pytest-asyncio:
pip install pytest-asyncio
import pytest
import asyncio
# Основной способ: @pytest.mark.asyncio
@pytest.mark.asyncio
async def test_async_function():
result = await some_async_function()
assert result == expected_value
2. Простые async функции
import asyncio
from typing import Coroutine
# Функция которую нужно тестировать
async def fetch_user(user_id: int) -> dict:
await asyncio.sleep(0.1) # Имитация запроса
return {"id": user_id, "name": "Alice"}
# Способ 1: с @pytest.mark.asyncio
@pytest.mark.asyncio
async def test_fetch_user():
user = await fetch_user(1)
assert user["name"] == "Alice"
assert user["id"] == 1
# Способ 2: ручное создание loop (старый способ)
def test_fetch_user_manual():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
user = loop.run_until_complete(fetch_user(1))
assert user["name"] == "Alice"
finally:
loop.close()
# Способ 3: asyncio.run() (Python 3.7+)
def test_fetch_user_run():
user = asyncio.run(fetch_user(1))
assert user["name"] == "Alice"
Тестирование HTTP запросов с aiohttp
Использование aioresponses (mock)
pip install aioresponses pytest-asyncio aiohttp
import aiohttp
import pytest
from aioresponses import aioresponses
# Функция которую тестируем
async def get_user_from_api(user_id: int) -> dict:
async with aiohttp.ClientSession() as session:
async with session.get(f"https://api.example.com/users/{user_id}") as resp:
return await resp.json()
# Тест с мокированием
@pytest.mark.asyncio
async def test_get_user_from_api():
# Мокируем HTTP ответ
with aioresponses() as mocked:
mocked.get(
"https://api.example.com/users/1",
payload={"id": 1, "name": "Alice", "email": "alice@example.com"},
status=200
)
result = await get_user_from_api(1)
assert result["name"] == "Alice"
assert result["email"] == "alice@example.com"
Тестирование с ошибками
@pytest.mark.asyncio
async def test_api_error_handling():
with aioresponses() as mocked:
# Мокируем ошибку 404
mocked.get(
"https://api.example.com/users/999",
status=404,
payload={"error": "Not found"}
)
# Ожидаем исключение
with pytest.raises(Exception):
await get_user_from_api(999)
Параллельное выполнение (asyncio.gather)
Тестирование параллельных задач
# Функция с параллельными запросами
async def fetch_multiple_users(user_ids: list[int]) -> list[dict]:
tasks = [fetch_user(uid) for uid in user_ids]
return await asyncio.gather(*tasks)
# Тест
@pytest.mark.asyncio
async def test_fetch_multiple_users():
with aioresponses() as mocked:
for i in range(1, 4):
mocked.get(
f"https://api.example.com/users/{i}",
payload={"id": i, "name": f"User{i}"}
)
users = await fetch_multiple_users([1, 2, 3])
assert len(users) == 3
assert users[0]["name"] == "User1"
Таймауты и отмена задач
asyncio.wait_for
@pytest.mark.asyncio
async def test_timeout():
# Функция может зависнуть
async def slow_function():
await asyncio.sleep(10)
return "done"
# Ограничиваем время выполнения
with pytest.raises(asyncio.TimeoutError):
await asyncio.wait_for(slow_function(), timeout=1)
@pytest.mark.asyncio
async def test_cancellation():
async def long_task():
try:
await asyncio.sleep(10)
except asyncio.CancelledError:
return "cancelled"
task = asyncio.create_task(long_task())
await asyncio.sleep(0.1)
task.cancel()
result = await task
assert result == "cancelled"
Fixture для async тестов
# conftest.py
import pytest
import asyncio
from typing import AsyncGenerator
# Fixture для базы данных
@pytest.fixture
async def async_db() -> AsyncGenerator:
"""Инициализация БД для теста"""
db = await connect_to_db()
await db.execute("CREATE TABLE users (id INT, name TEXT)")
yield db
await db.execute("DROP TABLE users")
await db.close()
# Использование fixture
@pytest.mark.asyncio
async def test_with_db(async_db):
await async_db.execute("INSERT INTO users VALUES (1, 'Alice')")
result = await async_db.fetch("SELECT * FROM users")
assert len(result) == 1
Тестирование WebSocket
import websockets
import pytest
from unittest.mock import AsyncMock, patch
# Функция которую тестируем
async def send_ws_message(uri: str, message: str):
async with websockets.connect(uri) as websocket:
await websocket.send(message)
response = await websocket.recv()
return response
# Тест с мокированием WebSocket
@pytest.mark.asyncio
async def test_websocket():
mock_ws = AsyncMock()
mock_ws.__aenter__.return_value = mock_ws
mock_ws.__aexit__.return_value = None
mock_ws.recv.return_value = "pong"
with patch('websockets.connect', return_value=mock_ws):
result = await send_ws_message("ws://localhost:8000", "ping")
assert result == "pong"
mock_ws.send.assert_called_with("ping")
Использование unittest.mock для async
from unittest.mock import AsyncMock, patch
import asyncio
# Функция с зависимостями
async def process_user(user_id: int, user_service):
user = await user_service.get_user(user_id)
return {"processed": user["name"].upper()}
# Мокируем async метод
@pytest.mark.asyncio
async def test_with_mock():
mock_service = AsyncMock()
mock_service.get_user = AsyncMock(return_value={"id": 1, "name": "alice"})
result = await process_user(1, mock_service)
assert result["processed"] == "ALICE"
mock_service.get_user.assert_called_once_with(1)
Тестирование контекстных менеджеров
class AsyncDatabaseConnection:
async def __aenter__(self):
await asyncio.sleep(0.1)
self.connected = True
return self
async def __aexit__(self, exc_type, exc, tb):
self.connected = False
async def query(self, sql):
if not self.connected:
raise RuntimeError("Not connected")
return [{"result": "data"}]
# Тест
@pytest.mark.asyncio
async def test_async_context_manager():
async with AsyncDatabaseConnection() as db:
result = await db.query("SELECT * FROM users")
assert result[0]["result"] == "data"
Конфигурация pytest.ini
[pytest]
# Автоматически добавляем asyncio loop для всех async тестов
asyncio_mode = auto
# Вместо @pytest.mark.asyncio можно просто написать async def
# но с автоматическим loop
Лучшие практики
✅ Делай так:
# 1. Используй pytest-asyncio
@pytest.mark.asyncio
async def test_async():
...
# 2. Мокируй внешние API
with aioresponses() as mocked:
mocked.get(...)
result = await function()
# 3. Используй AsyncMock для внутренних функций
mock = AsyncMock(return_value=data)
# 4. Тестируй таймауты
async with pytest.raises(asyncio.TimeoutError):
await asyncio.wait_for(func(), timeout=1)
# 5. Используй fixture для инициализации
@pytest.fixture
async def async_client():
...
❌ Не делай так:
# 1. Не используй asyncio.run() в async тестах
def test_async(): # синхронный тест
result = asyncio.run(async_func())
# Плохо! Используй @pytest.mark.asyncio
# 2. Не забывай await
async def test():
task = fetch_user(1) # ОШИБКА! нет await
# 3. Не мешай sync и async код
# Если используешь async, всё должно быть async
# 4. Не блокируй event loop
async def test():
time.sleep(1) # ПЛОХО! используй await asyncio.sleep(1)
Заключение
Тестирование async кода — это специализированный навык. Ключевые инструменты:
- pytest-asyncio — для запуска async тестов
- aioresponses — для мокирования HTTP
- AsyncMock — для мокирования внутренних функций
- asyncio.wait_for — для таймаутов
- pytest fixture — для инициализации
Помни: async код требует особого внимания к таймаутам, отмене задач и управлению ошибками!