Асинхронный тестовый клиент для FastApi

Получаю ошибку в тесте: AttributeError: 'async_generator' object has no attribute 'get'

Вот тест:

@pytest.mark.asyncio
async def test_get_profile_route(self, client: AsyncClient):
    response = await client.get("/api/v1/profile/2")
    assert response.status_code == 200

conftest.py:

import pytest
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from alembic import command
from alembic.config import Config
from config.database.db_helper import db_helper
from main import get_application
from src.models import Base

DATABASE_URL = "postgresql+asyncpg://test_user:password@localhost/test_db"


engine = create_async_engine(DATABASE_URL, echo=True)
AsyncTestingSessionLocal = sessionmaker(
    engine, class_=AsyncSession, expire_on_commit=False
)

@pytest.fixture(scope="session")
def app():
    _app = get_application()
    return _app

@pytest.fixture(scope="session")
def alembic_config():
    config = Config("alembic.ini")
    config.set_main_option("sqlalchemy.url", DATABASE_URL)
    return config

@pytest.fixture(scope="session")
async def prepare_database(alembic_config):
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)
        await conn.run_sync(Base.metadata.create_all)
    
    command.upgrade(alembic_config, "head")

@pytest.fixture(scope="session")
async def db_session(prepare_database):
    await prepare_database
    async with AsyncTestingSessionLocal() as session:
        async with session.begin():
            yield session

@pytest.fixture(scope='session')
async def client(app, db_session) -> AsyncClient:
    async def override_get_db():
        yield db_session
    app.dependency_overrides[db_helper.get_async_session] = override_get_db
    async with AsyncClient(app=app, base_url="http://test", transport=ASGITransport(app)) as c:
        yield c
    app.dependency_overrides.clear()

Ответы (1 шт):

Автор решения: insolor

Проблема в том, что фикстура у вас асинхронная, но pytest сам по себе про асинхронные фикстуры ничего не знает.

Вот минимальный пример с такой проблемой:

import pytest


@pytest.fixture
async def some_async_fixture():
    yield {"some": "data"}


@pytest.mark.asyncio
async def test_something(some_async_fixture):
    assert some_async_fixture.get("some") == "data"

При запуске теста падает с ошибкой (не обязательно всегда будет писать про get, просто подобрал пример, чтобы имя метода совпадало):

❯ pytest test.py
================================== test session starts ===================================
...
collected 1 item                                                                         

test.py F                                                                          [100%]

======================================== FAILURES ========================================
_____________________________________ test_something _____________________________________

some_async_fixture = <async_generator object some_async_fixture at 0x7f291e34a040>

    @pytest.mark.asyncio
    async def test_something(some_async_fixture):
>       assert some_async_fixture.get("some") == "data"
E       AttributeError: 'async_generator' object has no attribute 'get'

test.py:11: AttributeError
================================ short test summary info =================================
FAILED test.py::test_something - AttributeError: 'async_generator' object has no attribute 'get'
=================================== 1 failed in 0.06s ====================================

Вариант решения через замену декоратора фикстуры

Решается либо заменой декоратора у фикстуры на @pytest_asyncio.fixture:

import pytest
import pytest_asyncio


@pytest_asyncio.fixture
async def some_async_fixture():
    yield {"some": "data"}


@pytest.mark.asyncio
async def test_something(some_async_fixture):
    assert some_async_fixture.get("some") == "data"

Вывод:

❯ pytest test.py
================================== test session starts ===================================
...
collected 1 item                                                                         

test.py .                                                                          [100%]

=================================== 1 passed in 0.01s ====================================

Решение через конфигурацию

Либо без изменения кода проблему можно решить добавлением asyncio_mode = auto в конфиг pytest, либо в файл pytest.ini:

[pytest]
asyncio_mode = auto

либо в pyproject.toml:

[tool.pytest.ini_options]
asyncio_mode = "auto"

Кавычки вокруг auto имеют значение: в ini их не должно быть, в toml - должны быть.

После добавления этой опции добавление декоратора @pytest.mark.asyncio к асинхронным тестам становится необязательным.

→ Ссылка