Как использовать `pytest` для тестирования асинхронных функций?

Для тестирования асинхронных функций в `pytest` используйте следующие подходы:
  • `pytest-asyncio` plugin: Установите `pip install pytest-asyncio`. Он автоматически обнаруживает и запускает асинхронные тесты.
  • `async def` тесты: Определите тестовые функции с помощью `async def`. `pytest-asyncio` обеспечит их правильное выполнение в асинхронном контексте.
  • `@pytest.mark.asyncio`: Используйте этот декоратор для явного указания, что тест является асинхронным, если `pytest-asyncio` не распознал его автоматически.
  • `asyncio.run()` (в крайнем случае): Если нужно запустить асинхронную функцию в синхронном тесте, используйте `asyncio.run(coroutine)`. Но лучше стараться избегать этого и делать тесты асинхронными.
  • Фикстуры с `async def`: Фикстуры тоже могут быть асинхронными (определяются с помощью `async def`). `pytest-asyncio` позаботится об их правильном выполнении.
Пример:

import pytest
import asyncio

@pytest.mark.asyncio
async def test_async_function():
    async def my_async_func():
        await asyncio.sleep(0.1)
        return True

    result = await my_async_func()
    assert result == True
  

Для тестирования асинхронных функций с помощью pytest необходимо использовать библиотеку pytest-asyncio. Она предоставляет поддержку асинхронных тестов, позволяя pytest правильно обрабатывать корутины.

Установка pytest-asyncio:

pip install pytest-asyncio

Основные моменты использования:

  1. Асинхронные тестовые функции: Чтобы определить, что функция является асинхронным тестом, нужно использовать ключевое слово async.
    
    import pytest
    
    async def my_async_function():
        return "Hello, async world!"
    
    @pytest.mark.asyncio
    async def test_my_async_function():
        result = await my_async_function()
        assert result == "Hello, async world!"
          
  2. Декоратор @pytest.mark.asyncio: Этот декоратор помечает функцию как асинхронный тест. Некоторые версии pytest-asyncio требуют его явного указания, хотя в современных версиях он часто не является обязательным и pytest автоматически определяет асинхронные тесты. Лучше его использовать для явности и совместимости.
  3. Фикстуры pytest: Фикстуры могут быть асинхронными. Это позволяет выполнять асинхронные операции (например, подключение к асинхронной базе данных) до и после выполнения тестов.
    
    import pytest
    import asyncio
    
    @pytest.fixture(scope="session")
    async def async_db_connection():
        # Асинхронное подключение к базе данных
        print("Connecting to async database...")
        await asyncio.sleep(0.1) # Имитация асинхронной операции
        connection = "Async DB Connection"
        yield connection
        # Асинхронное закрытие соединения
        print("Closing async database connection...")
        await asyncio.sleep(0.1) # Имитация асинхронной операции
    
    
    @pytest.mark.asyncio
    async def test_using_async_fixture(async_db_connection):
        assert async_db_connection == "Async DB Connection"
        print("Test running with async connection")
          
  4. Использование asyncio.run() (в редких случаях): В большинстве случаев pytest-asyncio сам управляет event loop. Однако, если требуется выполнение синхронного кода, который вызывает асинхронные функции, можно использовать asyncio.run(). Важно понимать, что это обычно не требуется внутри тестов, а скорее в коде, который тестируется.
    
    import asyncio
    import pytest
    
    async def async_task(value):
      await asyncio.sleep(0.1)
      return value * 2
    
    def sync_function_calling_async(value):
      # ВНИМАНИЕ: Использование asyncio.run() в тестах обычно не рекомендуется
      #          Предпочтительнее, чтобы все тесты были асинхронными
      return asyncio.run(async_task(value))
    
    @pytest.mark.asyncio
    async def test_async_task():
      result = await async_task(5)
      assert result == 10
    
    def test_sync_function_calling_async(): # Обычный синхронный тест
      result = sync_function_calling_async(3)
      assert result == 6
          

    Важное замечание: Настоятельно рекомендуется избегать использования asyncio.run() внутри тестов. Лучше сделать все тесты асинхронными, чтобы pytest-asyncio мог корректно управлять event loop.

Пример полного тестового файла (test_async.py):


import pytest
import asyncio

async def fetch_data(url):
  await asyncio.sleep(0.01) # Имитация задержки сети
  return f"Data from {url}"

@pytest.mark.asyncio
async def test_fetch_data():
  url = "https://example.com"
  data = await fetch_data(url)
  assert data == f"Data from {url}"

@pytest.fixture
async def async_setup():
    print("Setting up async test environment...")
    await asyncio.sleep(0.01)
    yield
    print("Tearing down async test environment...")
    await asyncio.sleep(0.01)

@pytest.mark.asyncio
async def test_with_async_setup(async_setup):
    print("Running test with async setup")
    assert True # Просто пример assert
  

Запуск тестов:

pytest test_async.py

Преимущества использования pytest-asyncio:

  • Простая интеграция с pytest.
  • Автоматическая обработка event loop для асинхронных тестов.
  • Возможность использования асинхронных фикстур.
  • Чистый и понятный синтаксис.
0