Reputation: 94
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.
Upvotes: 2
Views: 1576
Reputation: 477
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
Upvotes: 1