tortoise / tortoise-orm

Familiar asyncio ORM for python, built with relations in mind
https://tortoise.github.io
Apache License 2.0
4.44k stars 362 forks source link

db_url parameter is initializer is ignored and db passed in register_tortoise is used instead #704

Open alexferrari88 opened 3 years ago

alexferrari88 commented 3 years ago

I'm trying to create a test with pytest for my FastAPI + Tortoise ORM app. To start, I am using the example provided in the documentation.

main.py:

from typing import List
from pydantic import UUID4
from fastapi import Depends, FastAPI, HTTPException, Request
from . import crud, models, schemas, auth
from tortoise.contrib.fastapi import HTTPNotFoundError, register_tortoise
from fastapi_users import FastAPIUsers
from .config import JWT_SECRET, ADMIN_PW
from .workers import selenium, jobs

app = FastAPI()

...

register_tortoise(
    app,
    db_url="sqlite://./db.sqlite3",
    modules={"models": ["app.models"]},
    generate_schemas=True,
    add_exception_handlers=True,
)

My test.py:

# mypy: no-disallow-untyped-decorators
# pylint: disable=E0611,E0401
import asyncio
from typing import Generator

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

from tortoise.contrib.test import finalizer, initializer

@pytest.fixture(scope="module")
def client() -> Generator:
    initializer(modules=["app.models"], db_url="sqlite://./test_db.sqlite3")
    with TestClient(app) as c:
        yield c
    finalizer()

@pytest.fixture(scope="module")
def event_loop(client: TestClient) -> Generator:
    yield client.task.get_loop()

def test_create_user(
    client: TestClient, event_loop: asyncio.AbstractEventLoop
):  # nosec
    response = client.post(
        "/auth/register",
        json={
            "email": "test@test.com",
            "password": "string",
            "is_active": True,
            "is_superuser": False,
            "is_verified": False,
        },
    )
    assert response.status_code == 201, response.text
    data = response.json()
    assert data["email"] == "test@test.com"
    assert "id" in data
    user_id = data["id"]

    async def get_user_by_db():
        user = await models.UserModel.get(id=user_id)
        return user

    user_obj = event_loop.run_until_complete(get_user_by_db())
    assert user_obj.id == user_id

Yet, the data is written to db.sqlite3 in the root folder and even not cleaned at the end of the test (the data remains in the database). I have also tried using sqlite://:memory: but to no avail. It always defaults to db.sqlite3.

Is this a bug or am I doing something wrong?

saintlyzero commented 3 years ago

Getting tests to work was a bit tricky
This code worked for me to setup fixtures

@pytest.fixture(scope="module")
def client():
    client = TestClient(app)
    yield client

@pytest.fixture(scope="module", autouse=True)
def initialize_tests(request):
    initializer(
        ["app.models"],
        db_url="sqlite://./test_db.sqlite3",
        app_label="test_app",
    )
    request.addfinalizer(finalizer)
rafsaf commented 3 years ago

Hi, I was thinking to start my own issue but I noticed that this one is already open, here are my thoughts.

Problem

In tortoise docs - fastapi tests.py example you can read stuff like import app from main.py in tests.py. BUT if one uses register_tortoise function in main.py, it does not seem to work (pytest).

There are another two simple and obvious solutions:

My understanding

It looks like for some reason it is trying to connect the default database declared in main.py instead of sqlite given in the initializer cuz of those @app.on_event("startup") and @app.on_event("shutdown") from register_tortoise. Maybe it should be mentioned somewhere in the documentation.

Cheers!

app/tests/conftest.py and app/main.py

# tests/conftest.py
from typing import Generator
from app.core.config import settings
import pytest
from fastapi.testclient import TestClient
from fastapi import FastAPI
from app.api.api import api_router
from tortoise.contrib.test import finalizer, initializer

# from app.main import app

# above line DOES NOT WORK PROPERLY, but when declaring app directly here, everything is ok
# another option is to delete register_tortoise stuff from main.py

app = FastAPI()

app.include_router(api_router, prefix=settings.API_STR)

@pytest.fixture(scope="module")
def client() -> Generator:
    initializer(["app.models"])
    with TestClient(app) as c:
        yield c
    finalizer()

@pytest.fixture(scope="module")
def event_loop(client: TestClient) -> Generator:
    yield client.task.get_loop()
# main.py
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
from tortoise.contrib.fastapi import register_tortoise

from app.api.api import api_router
from app.core.config import settings

app = FastAPI(
    title=settings.PROJECT_NAME, openapi_url=f"{settings.API_STR}/openapi.json"
)
register_tortoise(
    app=app,
    db_url=settings.TORTOISE_DATABASE_URI,
    modules={"models": ["app.models"]},
    add_exception_handlers=True,
)

# Set all CORS enabled origins
if settings.BACKEND_CORS_ORIGINS:
    app.add_middleware(
        CORSMiddleware,
        allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS],
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

app.include_router(api_router, prefix=settings.API_STR)
alexferrari88 commented 3 years ago

@rafsaf I have solved this issue making my main.py like this:

def create_app() -> FastAPI:
    app = FastAPI()
    ...
    return app

app = create_app()

register_tortoise(
    app,
    db_url=environ.get("DATABASE_URL"),
    modules={"models": ["app.models"]},
    generate_schemas=True,
    add_exception_handlers=True,
)
rafsaf commented 3 years ago

@alexferrari88 Nice, this one seems to be the same approach as mine, but you just wrote your code once rather than twice as i did above which is even better solution. Thanks for sharing.

DarioPanada commented 2 years ago

@alexferrari88 that worked for me too thanks!