Как тестировать асинхронный код?python-60

Тестирование асинхронного кода требует специальных подходов, отличающихся от тестирования синхронного кода. Рассмотрим основные методы и инструменты.

1. Использование pytest-asyncio

Плагин pytest-asyncio позволяет тестировать корутины напрямую.

pip install pytest-asyncio

Пример теста:

import pytest
import asyncio

@pytest.mark.asyncio
async def test_async_code():
    async def async_func():
        await asyncio.sleep(0.1)
        return 42

    result = await async_func()
    assert result == 42

2. Тестирование с помощью unittest.IsolatedAsyncioTestCase

Для тех, кто предпочитает unittest:

import unittest

class TestAsync(unittest.IsolatedAsyncioTestCase):
    async def test_async(self):
        async def async_func():
            return "result"

        self.assertEqual(await async_func(), "result")

3. Мокирование асинхронных функций

Использование AsyncMock для мокирования:

from unittest.mock import AsyncMock
import pytest

@pytest.mark.asyncio
async def test_async_mock():
    mock = AsyncMock(return_value=42)
    result = await mock()
    assert result == 42
    mock.assert_awaited_once()

4. Тестирование HTTP-клиентов

Пример с aiohttp:

import pytest
from aiohttp import web

async def hello(request):
    return web.Response(text="Hello")

@pytest.fixture
async def test_client(aiohttp_client):
    app = web.Application()
    app.router.add_get("/", hello)
    return await aiohttp_client(app)

@pytest.mark.asyncio
async def test_hello(test_client):
    resp = await test_client.get("/")
    assert resp.status == 200
    text = await resp.text()
    assert text == "Hello"

5. Тестирование с asyncio.gather

import pytest

@pytest.mark.asyncio
async def test_parallel():
    async def coro(x):
        return x * 2

    results = await asyncio.gather(
        coro(1),
        coro(2),
        coro(3)
    )
    assert results == [2, 4, 6]

6. Обработка исключений

@pytest.mark.asyncio
async def test_async_exception():
    async def raise_exc():
        raise ValueError("Error")

    with pytest.raises(ValueError, match="Error"):
        await raise_exc()

7. Таймауты в тестах

@pytest.mark.asyncio
async def test_timeout():
    async def long_running():
        await asyncio.sleep(10)
        return "OK"

    with pytest.raises(asyncio.TimeoutError):
        await asyncio.wait_for(long_running(), timeout=0.1)

8. Фикстуры для асинхронного кода

@pytest.fixture
async def async_fixture():
    await asyncio.sleep(0.1)
    return "fixture value"

@pytest.mark.asyncio
async def test_with_async_fixture(async_fixture):
    assert async_fixture == "fixture value"

9. Тестирование с asyncio.Event

@pytest.mark.asyncio
async def test_event():
    event = asyncio.Event()

    async def set_event():
        await asyncio.sleep(0.1)
        event.set()

    asyncio.create_task(set_event())
    await event.wait()
    assert event.is_set()

10. Продвинутые техники

a. Тестирование с asyncio.Queue

async def test_queue():
    queue = asyncio.Queue()
    await queue.put("item")
    assert await queue.get() == "item"

b. Патчинг асинхронных функций

from unittest.mock import patch

async def external_call():
    return "real"

@pytest.mark.asyncio
@patch("__main__.external_call", new_callable=AsyncMock)
async def test_patched(mock_call):
    mock_call.return_value = "mocked"
    assert await external_call() == "mocked"

Ошибки и подводные камни

  1. Забыть await в тестах
  2. Неправильная настройка event loop
  3. Использование синхронных моков для асинхронного кода
  4. Утечки ресурсов в фикстурах

Инструменты и библиотеки

  1. pytest-asyncio - основной плагин
  2. asynctest - альтернативный подход (устаревает)
  3. aiohttp - для тестирования HTTP-серверов/клиентов
  4. aiomysql/aiopg - для тестирования с БД

Резюмируем

Для эффективного тестирования асинхронного кода:

  1. Используйте pytest-asyncio или unittest.IsolatedAsyncioTestCase
  2. Применяйте AsyncMock для мокирования
  3. Тестируйте не только happy path, но и ошибки/таймауты
  4. Уделяйте внимание очистке ресурсов
  5. Используйте специализированные инструменты для HTTP/DB тестирования

Правильно написанные тесты асинхронного кода помогают выявлять проблемы с race conditions, deadlock'ами и другими асинхронными специфичными багами.