简体   繁体   中英

Issues testing a FastApi app using TortoiseOrm as a database layer

I am having issues with testing a FastApi app using TortoiseOrm as a database layer. Please find below a minimal reproducible example. The example is a small API with 2 endpoints that accept get requests, and return dummy data. One endpoint interacts with the database while the other does not.

My primary issue is that while both endpoints work perfectly when I interact with them on the browser or with Postman, the endpoint that interacts with the database throws an error during tests. Please find a full stack trace further below. For reasons I'm struggling to understand, it seems like TortoiseOrm can't establish a database connection during tests.

Tests are run with pytest using the command

pytest -v -s -p no:warnings

main.py

from fastapi import FastAPI, status
from db import Tournament
from db import create_start_app_handler


def get_application():
    app = FastAPI()
    app.add_event_handler("startup", create_start_app_handler(app))
    return app


app = get_application()


@app.get("/",
    name="point1",
    status_code=status.HTTP_200_OK
)
async def home():
    return {
        "title":"Hello world"
    }


@app.get(
    "/save/",
    name="point2",
    status_code=status.HTTP_200_OK
)
async def save_data():
    await Tournament.create(
        name="test2"
    )
    return {
        "status":"created"
    }

db.py

from fastapi import FastAPI
from tortoise.models import Model
from tortoise import fields
from tortoise.contrib.fastapi import register_tortoise
from typing import Callable


class Tournament(Model):
    id = fields.IntField(pk=True)
    name = fields.CharField(max_length=255)


async def init_db(app: FastAPI):
    register_tortoise(
        app,
        db_url='postgres://admin:admin@localhost:5432/postgres_test',
        modules={"models": ["db"]},
        generate_schemas=True,
        add_exception_handlers=True,
    ) 


def create_start_app_handler(app: FastAPI) -> Callable:
    async def start_app() -> None:
        await init_db(app)
    return start_app

test_main.py

import pytest
from fastapi.testclient import TestClient
from main import app


class TestBase:
    client = TestClient(app)

    def test_home(self) -> None:
        response = self.client.get(
            app.url_path_for('point1')
        )
        assert response.status_code == 200

    def test_save(self) -> None:
        response = self.client.get(
            app.url_path_for('point2')
        )
        assert response.status_code == 200

stack trace

_____________________________________________________________________ TestBase.test_save ______________________________________________________________________

self = <test_main.TestBase object at 0x7fb7fb5831f0>

    def test_save(self) -> None:
>       response = self.client.get(
            app.url_path_for('point2')
        )

test_main.py:16: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../venv/lib/python3.8/site-packages/requests/sessions.py:555: in get
    return self.request('GET', url, **kwargs)
../venv/lib/python3.8/site-packages/starlette/testclient.py:415: in request
    return super().request(
../venv/lib/python3.8/site-packages/requests/sessions.py:542: in request
    resp = self.send(prep, **send_kwargs)
../venv/lib/python3.8/site-packages/requests/sessions.py:655: in send
    r = adapter.send(request, **kwargs)
../venv/lib/python3.8/site-packages/starlette/testclient.py:243: in send
    raise exc from None
../venv/lib/python3.8/site-packages/starlette/testclient.py:240: in send
    loop.run_until_complete(self.app(scope, receive, send))
/home/unyime/anaconda3/lib/python3.8/asyncio/base_events.py:616: in run_until_complete
    return future.result()
../venv/lib/python3.8/site-packages/fastapi/applications.py:208: in __call__
    await super().__call__(scope, receive, send)
../venv/lib/python3.8/site-packages/starlette/applications.py:112: in __call__
    await self.middleware_stack(scope, receive, send)
../venv/lib/python3.8/site-packages/starlette/middleware/errors.py:181: in __call__
    raise exc from None
../venv/lib/python3.8/site-packages/starlette/middleware/errors.py:159: in __call__
    await self.app(scope, receive, _send)
../venv/lib/python3.8/site-packages/starlette/exceptions.py:82: in __call__
    raise exc from None
../venv/lib/python3.8/site-packages/starlette/exceptions.py:71: in __call__
    await self.app(scope, receive, sender)
../venv/lib/python3.8/site-packages/starlette/routing.py:580: in __call__
    await route.handle(scope, receive, send)
../venv/lib/python3.8/site-packages/starlette/routing.py:241: in handle
    await self.app(scope, receive, send)
../venv/lib/python3.8/site-packages/starlette/routing.py:52: in app
    response = await func(request)
../venv/lib/python3.8/site-packages/fastapi/routing.py:219: in app
    raw_response = await run_endpoint_function(
../venv/lib/python3.8/site-packages/fastapi/routing.py:152: in run_endpoint_function
    return await dependant.call(**values)
main.py:31: in save_data
    await Tournament.create(
../venv/lib/python3.8/site-packages/tortoise/models.py:1104: in create
    db = kwargs.get("using_db") or cls._choose_db(True)
../venv/lib/python3.8/site-packages/tortoise/models.py:1014: in _choose_db
    db = router.db_for_write(cls)
../venv/lib/python3.8/site-packages/tortoise/router.py:39: in db_for_write
    return self._db_route(model, "db_for_write")
../venv/lib/python3.8/site-packages/tortoise/router.py:31: in _db_route
    return get_connection(self._router_func(model, action))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <tortoise.router.ConnectionRouter object at 0x7fb7fb6272e0>, model = <class 'db.Tournament'>, action = 'db_for_write'

    def _router_func(self, model: Type["Model"], action: str):
>       for r in self._routers:
E       TypeError: 'NoneType' object is not iterable

../venv/lib/python3.8/site-packages/tortoise/router.py:18: TypeError
=================================================================== short test summary info ===================================================================
FAILED test_main.py::TestBase::test_save - TypeError: 'NoneType' object is not iterable
================================================================= 1 failed, 1 passed in 2.53s =================================================================

How do I fix this? You can find the full code on Github here .

I'm late to the party as usual, but nevertheless:

What causes the error: DB schemas have not been initialized.

In your specific case: You initialize tortoise via fastapi startup event. Startup event does not fire under tests by default and therefore tortoise doesn't get configured. So you need to create the TestClient inside the test function, for example using with TestClient(app) as client:

With this change your tests in the repo appear to pass.

See

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM