15 : Unit Testing FastAPI Routes

  

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":"testuser@nofoobar.com","password":"testing"}
    response = client.post("/users/",json.dumps(data))
    assert response.status_code == 200 
    assert response.json()["email"] == "testuser@nofoobar.com"
    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)

Prev: 14 : Our … Next: 16 : Post …
FastAPITutorial

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.

Contacts

Refunds:

Refund Policy
Social

Follow us on our social media channels to stay updated.

© Copyright 2022-23 Team FastAPITutorial