I'm developing a backend app with FastAPI connected to a PostgreSQL database and I'm a bit stuck and lost with the tests good practices. I read a lot stackoverflow posts and blogs but I'm not really used to backend development and I still doesn't really understand what is the best practice.
Knowing that I use SQLModel, it is suggested in the documentation to perform the tests with a DB SQLite in memory. The problem is that when I follow the explained approach, I am struck by the non-compatibility between PG and SQLite (about schemas). The point is that I am consuming an existing DB with several schemas and not just a public schema. So, when I run my tests, I encounter the error "schema pouetpouet does not exist".
Finally, the question is: What should I do to test my app ?
- Find a way to setup the compatibility between my prod Postgres DB and an in-memory SQLite DB ?
- Apply my tests on a preprod Postgres DB and try to cleanup the added/removed items ? (what I did actually but I don't think it is a really good practice)
- Setup a local Postgres server inside a Docker container ?
- Mock a DB with kind of a Dict in the pytest test file ?
- Use a third lib like testcontainers for exemple ?
- Don't do tests ?
After all, I'd like to do unit and integration tests so maybe there is not only one solution about my needs.
Here is a really simplified version of my project:
The architecture of my project: (Consider that there is an __ init__.py file in each folder)
app/
├── api/
│ ├── core/
│ │ ├── config.py #get the env settings and distribute it to the app
│ │ ├── .env
│ ├── crud/
│ │ ├── items.py #the CRUD functions called by the router
│ ├── db/
│ │ ├── session.py #the get_session function handling the db engine
│ ├── models/
│ │ ├── items.py #the SQLModel object def as is in the db
│ ├── routers/
│ │ ├── items.py #the routing system
│ ├── schemas/
│ │ ├── items.py #the python object def as it is used in the app
│ ├── main.py #the main app
├── tests/
│ ├── test_items.py #the pytest testing file
In the crud/items.py:
from fastapi.encoders import jsonable_encoder
from sqlmodel import Session, select
from api.models import Item
from api.schemas import ItemCreate
def get_item(db_session: Session, item_id: int) -> Item:
query = select(Item).where(Item.id == item_id)
return db_session.exec(query).first()
def create_new_item(db_session: Session, *, obj_input: ItemCreate) -> Item:
obj_in_data = jsonable_encoder(obj_input)
db_obj = Item(**obj_in_data)
db_session.add(db_obj)
db_session.commit()
db_session.refresh(db_obj)
return db_obj
In the db/session.py:
from sqlalchemy.engine import Engine
from sqlmodel import create_engine, Session
from api.core.config import settings
engine: Engine = create_engine(settings.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True)
def get_session() -> Session:
with Session(engine) as session:
yield session
In the models/items.py:
from sqlmodel import SQLModel, Field, MetaData
meta = MetaData(schema="pouetpouet") # https://github.com/tiangolo/sqlmodel/issues/20
class Item(SQLModel, table=True):
__tablename__ = "cities"
# __table_args__ = {"schema": "pouetpouet"}
metadata = meta
id: int = Field(primary_key=True, default=None)
city_name: str
In the routers/items.py:
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session
from api.crud import get_item, create_new_item
from api.db.session import get_session
from api.models import Item
from api.schemas import ItemRead, ItemCreate
router = APIRouter(prefix="/api/items", tags=["Items"])
@router.get("/{item_id}", response_model=ItemRead)
def read_item(
*,
db_session: Session = Depends(get_session),
item_id: int,
) -> Item:
item = get_item(db_session=db_session, item_id=item_id)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
return item
@router.post("/", response_model=ItemRead)
def create_item(
*,
db_session: Session = Depends(get_session),
item_input: ItemCreate,
) -> Item:
item = create_new_item(db_session=db_session, obj_input=item_input)
return item
In the schemas/items.py:
from typing import Optional
from sqlmodel import SQLModel
class ItemBase(SQLModel):
city_name: Optional[str] = None
class ItemCreate(ItemBase):
pass
class ItemRead(ItemBase):
id: int
class Config:
orm_mode: True
In the tests/test_items.py:
from fastapi.testclient import TestClient
from api.main import app
client = TestClient(app)
def test_create_item() -> None:
data = {"city_name": "Las Vegas"}
response = client.post("/api/items/", json=data)
assert response.status_code == 200
content = response.json()
assert content["city_name"] == data["city_name"]
assert "id" in content
ps: not being very experienced in backend development, do not hesitate to bring constructive remarks about my code if you notice something strange. It will be very well received.