Message!
This is an old work, for updated version please visit Updated FastAPI Course
I was typing this post and it was 90% complete and then I mistakenly closed the tab and lost my work 😠This time I am using an extension that is autosaving and thus saving me!
So, today we are to discuss unit-testing. Many of you might think that why I did not write a test before actually making the user creation route? It's because I have seen my colleagues struggling with TDD and finally losing interest. Instead, I want to develop your interest in development, Once interested you yourself will follow the best practices.
Is TDD important ? Without a second thought, I would say yes, In our company codebase we have around 1400+ unit tests. Just 2 years before, We were literally fearful each time we had to release a new feature. Because If I make one single change in the Users table, I don't know which part of our codebase may break! I use unit tests mostly for a single purpose. It works as documentation from my side, By designing a unit test I tell fellow developers, how exactly this new feature should work.
Ok enough talk, I don't want to bore you by just talking. So, let's jump into it. We need to create some files and folders:
backend/
├─.env
├─apis/
│ ├─base.py
│ └─version1/
│ ├─route_general_pages.py
│ └─route_users.py
├─core/
│ ├─config.py
│ └─hashing.py
├─main.py
├─requirements.txt
├─templates/
│ ├─components/
│ │ └─navbar.html
│ ├─general_pages/
│ │ └─homepage.html
│ └─shared/
│ └─base.html
├─tests/ #new
│ ├─conftest.py #new
│ └─test_routes/ #new
│ └─test_users.py #new
We also need pytest and requests for testing our APIs. So, let's modify our requirements.txt file and do a pip install -r requirements.txt to install these.
#requirements.txt file
...
...
#for email validation
pydantic[email]
#hashing
passlib[bcrypt]
#for testing #new
pytest
requests
Now, we will add configurations for testing. Paste the following lines in tests > conftest.py
from typing import Any
from typing import Generator
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
#this is to include backend dir in sys.path so that we can import from db,main.py
from db.base import Base
from db.session import get_db
from apis.base import api_router
def start_application():
app = FastAPI()
app.include_router(api_router)
return app
SQLALCHEMY_DATABASE_URL = "sqlite:///./test_db.db"
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
# Use connect_args parameter only with sqlite
SessionTesting = sessionmaker(autocommit=False, autoflush=False, bind=engine)
@pytest.fixture(scope="function")
def app() -> Generator[FastAPI, Any, None]:
"""
Create a fresh database on each test case.
"""
Base.metadata.create_all(engine) # Create the tables.
_app = start_application()
yield _app
Base.metadata.drop_all(engine)
@pytest.fixture(scope="function")
def db_session(app: FastAPI) -> Generator[SessionTesting, Any, None]:
connection = engine.connect()
transaction = connection.begin()
session = SessionTesting(bind=connection)
yield session # use the session in tests.
session.close()
transaction.rollback()
connection.close()
@pytest.fixture(scope="function")
def client(
app: FastAPI, db_session: SessionTesting
) -> Generator[TestClient, Any, None]:
"""
Create a new FastAPI TestClient that uses the `db_session` fixture to override
the `get_db` dependency that is injected into routes.
"""
def _get_test_db():
try:
yield db_session
finally:
pass
app.dependency_overrides[get_db] = _get_test_db
with TestClient(app) as client:
yield client
Now, we can make unit tests, Notice we have made 'client' as a module-level test fixture. So, by using this client we would be able to rollback things and keep our tests isolated and independent. Type the below code in tests > test_routes > test_users.py
import json
def test_create_user(client):
data = {"username":"testuser","email":"[email protected]","password":"testing"}
response = client.post("/users/",json.dumps(data))
assert response.status_code == 200
assert response.json()["email"] == "[email protected]"
assert response.json()["is_active"] == True
Done, now type pytest
in the terminal/cmd and see the magic !
Final git commit : Configure unit test settings · nofoobar/JobBoard-Fastapi@b24ffe2 (github.com)
Brige the gap between Tutorial hell and Industry. We want to bring in the culture of Clean Code, Test Driven Development.
We know, we might make it hard for you but definitely worth the efforts.
© Copyright 2022-23 Team FastAPITutorial