I have a backend app developed with FastAPI, using SQLModel (SQLAlchemy & Pydantic) and connected to a Postgres database. I have integration tests to test if my endpoints are working fine with a stagging PG DB. But right now I have to write units tests and I don't know how to proceed to test my endpoints and the functions called in a isolated way.
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/ #the pytest tests
│ ├── unit_tests/
│ ├── integration_tests/
│ │ ├── test_items.py
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/integration_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
The point here is that I feel stuck with the db_session: Session
argument used in all the functions in the crud/items.py and the routers/items.py because I think it is mandatory to get a valid session of a valid postgres connexion for the tests.
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.