httpx.AsyncClient 的 pytest 找不到新创建的数据库记录

pytest with httpx.AsyncClient cannot find newly created database records

我正在尝试使用 httpx.AsyncClient 设置 pytest 并使用 FastAPI 设置 sqlalchemy AsyncSession。除了异步内容外,所有内容实际上都模仿了 FastAPI Fullstack repo 中的测试。

CRUD 单元测试没有问题。 运行 API 使用来自 httpx 库的 AsyncClient 进行测试时会出现此问题。

问题是,客户端发出的任何请求只能访问在初始化(设置)客户端装置之前创建的用户(在我的例子中)。

我的 pytest conftest.py 设置是这样的:

from typing import Dict, Generator, Callable
import asyncio
from fastapi import FastAPI
import pytest
# from sqlalchemy.orm import Session
from sqlalchemy.ext.asyncio import AsyncSession
from httpx import AsyncClient
import os
import warnings
import sqlalchemy as sa
from alembic.config import Config
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.orm import sessionmaker


async def get_test_session() -> Generator:
    test_engine = create_async_engine(
            settings.SQLALCHEMY_DATABASE_URI + '_test',
            echo=False,
        )
        
    # expire_on_commit=False will prevent attributes from being expired
    # after commit.
    async_sess = sessionmaker(
        test_engine, expire_on_commit=False, class_=AsyncSession
    )
    async with async_sess() as sess, sess.begin():    
        yield sess

@pytest.fixture(scope="session")
async def async_session() -> Generator:
    test_engine = create_async_engine(
            settings.SQLALCHEMY_DATABASE_URI + '_test',
            echo=False,
            pool_size=20, max_overflow=0
        )
        
    # expire_on_commit=False will prevent attributes from being expired
    # after commit.
    async_sess = sessionmaker(
        test_engine, expire_on_commit=False, class_=AsyncSession
    )
    yield async_sess

@pytest.fixture(scope="session")
async def insert_initial_data(async_session:Callable):
    async with async_session() as session, session.begin():
        # insert first superuser - basic CRUD ops to insert data in test db
        await insert_first_superuser(session)
        # insert test.superuser@example.com

        await insert_first_test_user(session)
        # inserts test.user@example.com

@pytest.fixture(scope='session')
def app(insert_initial_data) -> FastAPI:
    return  FastAPI()


@pytest.fixture(scope='session')
async def client(app: FastAPI) -> Generator:
    from app.api.deps import get_session
    
    app.dependency_overrides[get_session] = get_test_session
 
    async with AsyncClient(
                app=app, base_url="http://test", 
                ) as ac:
        yield ac

    # reset dependencies
    app.dependency_overrides = {}

所以在这种情况下,在 运行 API 测试期间只有超级用户 test.superuser@example.com 和普通用户 test.user@example.com 可用。例如,下面的代码能够很好地获取访问令牌:

async def authentication_token_from_email(
    client: AsyncClient,  session: AsyncSession,
) -> Dict[str, str]:
    """
    Return a valid token for the user with given email.

    
    """
    
    email = 'test.user@example.com'
    password = 'test.user.password'
    
    user = await crud.user.get_by_email(session, email=email)
    assert user is not None
    
    
    data = {"username": email, "password": password}

    response = await client.post(f"{settings.API_V1_STR}/auth/access-token", 
                                 data=data)
    auth_token = response.cookies.get('access_token')
    assert auth_token is not None

    return auth_token

但是,下面修改后的代码不会 - 这里我尝试插入新用户,然后登录以获取访问令牌。

async def authentication_token_from_email(
    client: AsyncClient, session: AsyncSession,
) -> Dict[str, str]:
    """
    Return a valid token for the user with given email.
    If the user doesn't exist it is created first.

    """
    
    email = random_email()
    password = random_lower_string()

    
    user = await crud.user.get_by_email(session, email=email)
    if not user:
        user_in_create = UserCreate(email=email, 
                                    password=password)
        user = await crud.user.create(session, obj_in=user_in_create)

        
    else:
        user_in_update = UserUpdate(password=password)
        user = await crud.user.update(session, db_obj=user, obj_in=user_in_update)

    assert user is not None

    # works fine up to this point, user inserted successfully
    # now try to send http request to fetch token, and user is not found in the db
        
    data = {"username": email, "password": password}
    response = await client.post(f"{settings.API_V1_STR}/auth/access-token", 
                                   data=data)
    auth_token = response.cookies.get('access_token')
    # returns None. 

    return auth_token

这是怎么回事?感谢任何帮助!

原来我需要做的就是在客户端 fixture 中定义 FastAPI 依赖覆盖函数:

之前


async def get_test_session() -> Generator:
    test_engine = create_async_engine(
            settings.SQLALCHEMY_DATABASE_URI + '_test',
            echo=False,
        )
        
    # expire_on_commit=False will prevent attributes from being expired
    # after commit.
    async_sess = sessionmaker(
        test_engine, expire_on_commit=False, class_=AsyncSession
    )
    async with async_sess() as sess, sess.begin():    
        yield sess

@pytest.fixture(scope='session')
async def client(app: FastAPI) -> Generator:
    from app.api.deps import get_session
    
    app.dependency_overrides[get_session] = get_test_session
 
    async with AsyncClient(
                app=app, base_url="http://test", 
                ) as ac:
        yield ac

    # reset dependencies
    app.dependency_overrides = {}

之后


@pytest.fixture(scope="function")
async def session(async_session) -> Generator:
    async with async_session() as sess, sess.begin():
        yield sess
        


@pytest.fixture
async def client(app: FastAPI, session:AsyncSession) -> Generator:
    from app.api.deps import get_session
    
    # this needs to be defined inside this fixture
    # this is generate that yields session retrieved from `session` fixture
    
    def get_sess(): 
        yield session
    
    app.dependency_overrides[get_session] =  get_sess
        
    async with AsyncClient(
             app=app, base_url="http://test", 
             ) as ac:
        yield ac

    app.dependency_overrides = {}

如果能对此行为做出任何解释,我将不胜感激。谢谢!