Test Driven Development (TDD) using FastAPI and Postgres

Writing test cases as we develop modules of API

ยท

7 min read

In this post, I'd discuss the Test Driven Development (TDD) strategy of developing APIs in Python using the FastAPI library. It is a methodology where you write test cases for your API end-points first, and then develop controller functions for the corresponding routes and make the test cases pass.

Importance of Testing

Testing is one of the most easily ignored concepts while developing the architecture of the software. Many companies/individual developers do not emphasize writing test cases citing various reasons why it would be redundant to the codebase. While it might be true in some cases, I believe it is an integral part of the software development cycle and definitely would be rewarding in the long run. Benefits start to surface for larger projects with many contributors as it becomes easier to identify bugs early. A common challenge we face in deciding test cases is, what should be covered and what should not. We do need not to cover all the possible scenarios but, it definitely helps writing test cases under certain cases. For instance, we can have a test case to test the database connection with our application.

Test Driven Development (TDD) - Introduction

It is a software development process relying on software requirements being converted to test cases before the software is fully developed, and tracking all software development by repeatedly testing the software against all test cases. This is as opposed to software being developed first and test cases created later.

For clarity, let's say we have a requirement of performing a CRUD operation on a given database table say 'items'. We would write test cases for creating, updating, getting and deleting the items before we go on to create actual functions which perform these actions. But, these processes can also be performed simultaneously.

TDD in FastAPI

FastAPI is a library in Python similar to Django and Flask used to create powerful APIs. I had written test cases for Django earlier, and recently wrote test cases for one of the applications we have in our organization written in FastAPI. It was easier for me to write test cases in FastAPI, in general everything is easier in FastAPI. That is one of the many reasons it is fast gaining popularity among back-end developers who prefer using Python. At the time of writing this post, the Github repo of FastAPI has 53k+ stars. In this post, I'd be writing test cases for 'tasks' module. This API would have a multi-user authentication system, upon login it would allow users to perform CRUD operations on task. Just a note before reading further, I am expecting some familiarity with FastAPI so I would not be including all the files here. However, I'd put the link to the repository at the end of the post for your reference. Below is the model for tasks that we'd use for testing.

from datetime import datetime
from sqlalchemy.orm import relationship
from sqlalchemy import Column, Float, String, ForeignKey, Text, DateTime, Integer

from backend.db import Base

class Task(Base):
    __tablename__ = "task"

    id = Column(Integer, primary_key=True, autoincrement=True)
    createdDate = Column(DateTime, default=datetime.now)
    title = Column(String(50))
    description = Column(Text)
    status = Column(String(50))
    owner_id = Column(Integer, ForeignKey("user.id", ondelete="CASCADE"))

    owner = relationship("User", back_populates="tasks")

We'd configure database settings to be used for our test case. Then, we'd write a test case to see if we're able to successfully log in as an authenticated user and get the access token. We'd include this token in our test cases for the tasks module. You'd often have a separate dummy database for testing, but in this case, I'd be using the same database, no separate database connection would be setup for testing to keep things simple. Create a folder called tests inside your project folder. Create a file called 'test_task.py', naming our files prefixed with 'test' would help identify files to look for running test cases. We'd use the TestClient function from FastAPI to perform testing.

from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

# Login with existing user
def test_auth_user():
    response = client.post(
        "auth/login/",
        json={
            "email": "deadpool@example.com",  
            "password": "chimichangas4life", 
        },
    )
    data = response.json()
    assert 'access_token' in data
    assert response.status_code == 200

Below is the code for db.py file used to setup the database connection using Postgres.

from sqlalchemy import create_engine, MetaData
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

import config

DATABASE_USERNAME = config.DATABASE_USERNAME
DATABASE_PASSWORD = config.DATABASE_PASSWORD
DATABASE_HOST = config.DATABASE_HOST
DATABASE_NAME = config.DATABASE_NAME

SQLALCHEMY_DATABASE_URL = f"postgresql://{DATABASE_USERNAME}:{DATABASE_PASSWORD}@{DATABASE_HOST}/{DATABASE_NAME}"

engine = create_engine(SQLALCHEMY_DATABASE_URL)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()
metadata = MetaData()

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

This is how the main.py file looks like.

from fastapi import FastAPI
import uvicorn

from backend.auth import router as auth_router
from backend.tasks import router as task_router

app = FastAPI(title="Fast API Blog",
    docs_url="/scrum-master-docs",
    version="0.0.1")

app.include_router(auth_router.router)
app.include_router(task_router.router)

@app.get('/')
def main_response():
    return {
        'message': 'API Success'
    }

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

Remember we already have a test file above inside the tests folder. Let's run test cases now using pytest library. Move ahead with pip installation if you don't have pytest installed. Then, run the following command from the root of your project

python -m pytest

You should see one test case passed on the terminal if everything goes fine. Let's add more test cases for tasks module.

from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

# Login with existing user
def test_auth_user():
    response = client.post(
        "auth/login/",
        json={
            "email": "deadpool@example.com",  
            "password": "chimichangas4life", 
        },
    )
    data = response.json()
    assert 'access_token' in data
    assert response.status_code == 200

def test_get_tasks():
    response = client.post(
        "auth/login/",
        json={
            "email": "deadpool@example.com",  
            "password": "chimichangas4life", 
        },
    )
    data = response.json()
    access_token = data['access_token']
    assert 'access_token' in data
    assert response.status_code == 200

    taskResponse = client.get(
        "task/",
        headers={
            "Authorization": "Bearer " + access_token
        }
    )
    data = taskResponse.json()
    assert taskResponse.status_code == 200


def test_create_tasks():
    response = client.post(
        "auth/login/",
        json={
            "email": "deadpool@example.com",  
            "password": "chimichangas4life", 
        },
    )
    data = response.json()
    access_token = data['access_token']
    assert 'access_token' in data
    assert response.status_code == 200

    taskResponse = client.post(
        "task/",
        json={
            "title": "Some Task", 
            "description": "Some task description", 
            "status": "In Progress"
        },
        headers={
            "Authorization": "Bearer " + access_token
        }
    )
    data = taskResponse.json()
    assert taskResponse.status_code == 201

I've written test cases above to test creation of tasks and fetching all tasks for a given user. Since it is a protected route, we'd request to login route first and get the token. Let's add test cases for updating and deleting a task.

def test_delete_task():
    response = client.post(
        "auth/login/",
        json={
            "email": "deadpool@example.com",  
            "password": "chimichangas4life", 
        },
    )
    data = response.json()
    access_token = data['access_token']
    assert 'access_token' in data
    assert response.status_code == 200

    removed_id = 9

    taskResponse = client.delete(f"/task/{removed_id}", 
        headers={
            "Authorization": "Bearer " + access_token
        }
    )
    assert taskResponse.status_code == 204

def test_update_task():
    response = client.post(
        "auth/login/",
        json={
            "email": "deadpool@example.com",  
            "password": "chimichangas4life", 
        },
    )
    data = response.json()
    access_token = data['access_token']
    assert 'access_token' in data
    assert response.status_code == 200
    updated_id = 10

    taskResponse = client.patch(f"/task/{updated_id}",
        json={
            "title": "Some Task now updated", 
            "description": "Some task description now updated",
        }, 
        headers={
            "Authorization": "Bearer " + access_token
        }
    )
    data = taskResponse.json()
    assert data['id'] == updated_id
    assert data['title'] == "Some Task now updated"
    assert data['description'] == "Some task description now updated"
    assert response.status_code == 200

Here, we need to identify the id of the task to be deleted/modified. We have used assert statements for the update test case to confirm our object was successfully updated with the values we passed in the patch request. For the delete request, we simply checked the status code for 204.

To keep this post concise, I've omitted some pieces of code as I wanted to concentrate on writing test cases only. For reference, you can check this repository - https://github.com/Apfirebolt/Fastapi-scrum-master

This is a kanban drag-and-drop application that I created using FastAPI and React.

Conclusion

In this post, I discussed the importance of testing, test-driven-development and how to implement it using FastAPI and Postgres. Please visit the repository for in-depth dive into other modules referenced in this post like authentication. Feel free to share your views in the comments section. That is it for now people, I will see you on another cool topic next time. Till then,

Stay Safe..! Keep Learning..! ๐Ÿ˜ƒ

ย