使用 starlette 配置的 Fastapi 数据库测试隔离

Posted

技术标签:

【中文标题】使用 starlette 配置的 Fastapi 数据库测试隔离【英文标题】:Fastapi database test isolation with the starlette configuration 【发布时间】:2021-04-04 19:52:57 【问题描述】:

如何为测试目的准确配置数据库。我做错了什么,我已经阅读了网站上的 starlette 配置,但我认为我没有正确理解它。我尝试构建它,但收到以下错误消息:


tests\test_main.py .                                                                                                                        [ 33%]
tests\test_players\test_players.py FF                                                                                                       [100%]

==================================================================== FAILURES ====================================================================
________________________________________________________________ test_post_player ________________________________________________________________

test_app = <httpx.AsyncClient object at 0x0000021B01F8C2E0>

    @pytest.mark.asyncio
    async def test_post_player(test_app):
        test_request_payload = "first_name": "Jane", "last_name": "Doe", "nationality": "American", "date_of_birth": "1985-04-15"
        test_response_payload = "first_name": "Jane", "last_name": "Doe", "nationality": "American"


>       response = await test_app.post("/api/players/", data=json.dumps(test_request_payload),)

tests\test_players\test_players.py:12:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
venv\lib\site-packages\httpx\_client.py:1633: in post
    return await self.request(
venv\lib\site-packages\httpx\_client.py:1371: in request
    response = await self.send(
venv\lib\site-packages\httpx\_client.py:1406: in send
    response = await self._send_handling_auth(
venv\lib\site-packages\httpx\_client.py:1444: in _send_handling_auth
    response = await self._send_handling_redirects(
venv\lib\site-packages\httpx\_client.py:1476: in _send_handling_redirects
    response = await self._send_single_request(request, timeout)
venv\lib\site-packages\httpx\_client.py:1502: in _send_single_request
    (status_code, headers, stream, ext,) = await transport.arequest(
venv\lib\site-packages\httpx\_transports\asgi.py:148: in arequest
    await self.app(scope, receive, send)
venv\lib\site-packages\fastapi\applications.py:199: in __call__
    await super().__call__(scope, receive, send)
venv\lib\site-packages\starlette\applications.py:111: in __call__
    await self.middleware_stack(scope, receive, send)
venv\lib\site-packages\starlette\middleware\errors.py:181: in __call__
    raise exc from None
venv\lib\site-packages\starlette\middleware\errors.py:159: in __call__
    await self.app(scope, receive, _send)
venv\lib\site-packages\starlette\middleware\cors.py:78: in __call__
    await self.app(scope, receive, send)
venv\lib\site-packages\starlette\exceptions.py:82: in __call__
    raise exc from None
venv\lib\site-packages\starlette\exceptions.py:71: in __call__
    await self.app(scope, receive, sender)
venv\lib\site-packages\starlette\routing.py:566: in __call__
    await route.handle(scope, receive, send)
venv\lib\site-packages\starlette\routing.py:227: in handle
    await self.app(scope, receive, send)
venv\lib\site-packages\starlette\routing.py:41: in app
    response = await func(request)
venv\lib\site-packages\fastapi\routing.py:201: in app
    raw_response = await run_endpoint_function(
venv\lib\site-packages\fastapi\routing.py:148: in run_endpoint_function
    return await dependant.call(**values)
src\app\api\routers\players\players.py:11: in post_player
    player = await crudplayers.create_player(payload)
src\app\crud\crudplayers\crudplayers.py:13: in create_player
    await database.execute(query=query)
venv\lib\site-packages\databases\core.py:160: in execute
    async with self.connection() as connection:
venv\lib\site-packages\databases\core.py:219: in __aenter__
    await self._connection.acquire()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <databases.backends.postgres.PostgresConnection object at 0x0000021B01F980D0>

    async def acquire(self) -> None:
        assert self._connection is None, "Connection is already acquired"
>       assert self._database._pool is not None, "DatabaseBackend is not running"
E       AssertionError: DatabaseBackend is not running

venv\lib\site-packages\databases\backends\postgres.py:148: AssertionError
________________________________________________________________ test_read_player ________________________________________________________________

test_app = <httpx.AsyncClient object at 0x0000021B02374EB0>

    @pytest.mark.asyncio
    async def test_read_player(test_app):
        test_response_payload = "id": 1, "first_name": "Jane", "last_name": "Doe", "nationality": "American"
>       response = await test_app.get("/api/players/1")

tests\test_players\test_players.py:21:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
venv\lib\site-packages\httpx\_client.py:1548: in get
    return await self.request(
venv\lib\site-packages\httpx\_client.py:1371: in request
    response = await self.send(
venv\lib\site-packages\httpx\_client.py:1406: in send
    response = await self._send_handling_auth(
venv\lib\site-packages\httpx\_client.py:1444: in _send_handling_auth
    response = await self._send_handling_redirects(
venv\lib\site-packages\httpx\_client.py:1476: in _send_handling_redirects
    response = await self._send_single_request(request, timeout)
venv\lib\site-packages\httpx\_client.py:1502: in _send_single_request
    (status_code, headers, stream, ext,) = await transport.arequest(
venv\lib\site-packages\httpx\_transports\asgi.py:148: in arequest
    await self.app(scope, receive, send)
venv\lib\site-packages\fastapi\applications.py:199: in __call__
    await super().__call__(scope, receive, send)
venv\lib\site-packages\starlette\applications.py:111: in __call__
    await self.middleware_stack(scope, receive, send)
venv\lib\site-packages\starlette\middleware\errors.py:181: in __call__
    raise exc from None
venv\lib\site-packages\starlette\middleware\errors.py:159: in __call__
    await self.app(scope, receive, _send)
venv\lib\site-packages\starlette\middleware\cors.py:78: in __call__
    await self.app(scope, receive, send)
venv\lib\site-packages\starlette\exceptions.py:82: in __call__
    raise exc from None
venv\lib\site-packages\starlette\exceptions.py:71: in __call__
    await self.app(scope, receive, sender)
venv\lib\site-packages\starlette\routing.py:566: in __call__
    await route.handle(scope, receive, send)
venv\lib\site-packages\starlette\routing.py:227: in handle
    await self.app(scope, receive, send)
venv\lib\site-packages\starlette\routing.py:41: in app
    response = await func(request)
venv\lib\site-packages\fastapi\routing.py:201: in app
    raw_response = await run_endpoint_function(
venv\lib\site-packages\fastapi\routing.py:148: in run_endpoint_function
    return await dependant.call(**values)
src\app\api\routers\players\players.py:18: in read_player
    player = await crudplayers.get_player(player_id)
src\app\crud\crudplayers\crudplayers.py:19: in get_player
    player = await database.fetch_one(query=query)
venv\lib\site-packages\databases\core.py:145: in fetch_one
    async with self.connection() as connection:
venv\lib\site-packages\databases\core.py:219: in __aenter__
    await self._connection.acquire()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <databases.backends.postgres.PostgresConnection object at 0x0000021B023757F0>

    async def acquire(self) -> None:
        assert self._connection is None, "Connection is already acquired"
>       assert self._database._pool is not None, "DatabaseBackend is not running"
E       AssertionError: DatabaseBackend is not running

venv\lib\site-packages\databases\backends\postgres.py:148: AssertionError
================================================================ warnings summary ================================================================
venv\lib\site-packages\aiofiles\os.py:10
venv\lib\site-packages\aiofiles\os.py:10
venv\lib\site-packages\aiofiles\os.py:10
venv\lib\site-packages\aiofiles\os.py:10
venv\lib\site-packages\aiofiles\os.py:10
  c:\users\mathiaskolie\pycharmprojects\microservices\livegolf\venv\lib\site-packages\aiofiles\os.py:10: DeprecationWarning: "@coroutine" decorator
 is deprecated since Python 3.8, use "async def" instead
    def run(*args, loop=None, executor=None, **kwargs):

-- Docs: https://docs.pytest.org/en/stable/warnings.html
============================================================ short test summary info =============================================================
FAILED tests/test_players/test_players.py::test_post_player - AssertionError: DatabaseBackend is not running
FAILED tests/test_players/test_players.py::test_read_player - AssertionError: DatabaseBackend is not running
==================================================== 2 failed, 1 passed, 5 warnings in 7.23s =====================================================

conftest.py 中的 Starlette 配置

import pytest
from httpx import AsyncClient
from starlette.config import environ
from sqlalchemy import create_engine
from sqlalchemy_utils import database_exists, create_database, drop_database
from src.app.domain.models.models import metadata

environ['TESTING'] = 'True'

from src.app.main import app
from src.app.core.config import TEST_DATABASE_URL


@pytest.fixture(scope="function", autouse=True)
async def create_test_database():
    url = str(TEST_DATABASE_URL)
    engine = create_engine(url)
    create_database(url)
    metadata.create_all(engine)
    yield                    **#it returns None**
    drop_database(url) 


@pytest.fixture(scope="function")
async def test_app():
    async with AsyncClient(app=app, base_url="http://localhost:8000", headers="Content-Type": "application/json",) as ac:
        yield ac

数据库配置

from starlette.datastructures import Secret, CommaSeparatedStrings
from starlette.config import Config
from pydantic import BaseSettings
from typing import List
from databases import DatabaseURL
from sqlalchemy import create_engine



class Settings(BaseSettings):
    POSTGRES_USER: str
    POSTGRES_PASSWORD: str
    POSTGRES_HOST:str
    POSTGRES_PORT: int
    DATABASE_NAME: str
    ALLOWED_HOSTS: List[str]
    TESTING: str
    TESTING_DATABASE_NAME: str

    class Config:
        env_file = "src/app/core/.env"


config = Config(Settings.Config.env_file)

SECRET_KEY = config('SECRET_KEY', cast=Secret)

TESTING = config('TESTING', cast=bool, default=False)
POSTGRES_USER = config('POSTGRES_USER')
POSTGRES_PASSWORD = config('POSTGRES_PASSWORD', cast=Secret)
POSTGRES_HOST = config('POSTGRES_HOST')
POSTGRES_PORT = config('POSTGRES_PORT', cast=int, default=5432)
DATABASE_NAME = config('DATABASE_NAME')
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=CommaSeparatedStrings)
TESTING_DATABASE_NAME = config('TESTING_DATABASE_NAME')


DATABASE_URL = config(
    "DATABASE_URL",
    default=f"postgresql://POSTGRES_USER:POSTGRES_PASSWORD@POSTGRES_HOST:POSTGRES_PORT/DATABASE_NAME",
)

TEST_DATABASE_URL = config(
    "TEST_DATABASE_URL",
    default=f"postgresql://POSTGRES_USER:POSTGRES_PASSWORD@POSTGRES_HOST:POSTGRES_PORT/TESTING_DATABASE_NAME",
)

engine = create_engine(DATABASE_URL, pool_size=3, max_overflow=0)

【问题讨论】:

我在这里找到了使用 LifespanManager 的解决方案 github.com/encode/starlette/issues/104。 test_app 固定装置必须按照说明进行更新。 【参考方案1】:

我找到了这个解决方案here: “在上下文管理器中使用您的 TestClient”

@pytest.fixture(scope="module")
def client() -> Generator:
    with TestClient(api) as c:
        yield c

对我有同样问题的好作品

【讨论】:

以上是关于使用 starlette 配置的 Fastapi 数据库测试隔离的主要内容,如果未能解决你的问题,请参考以下文章

FastAPI Web框架 [Pydantic]

FastAPI Web框架 [Pydantic]

FastAPI Web框架 [Pydantic]

FastAPI websocket ping/pong 超时

三万字长文让你彻底掌握 FastAPI

Fastapi + 草莓 GraphQL