This content originally appeared on Level Up Coding - Medium and was authored by Rukshan J. Senanayaka
Introduction
Test-Driven Development (TDD) is a software development approach that emphasizes writing tests before implementing the actual code.
This article demonstrates TDD using Python, FastAPI, and pytest to build a Todo application with CRUD functionality. We’ll use Poetry for dependency management and PyCharm as our development environment. All of these tools are usable for pretty much any project, specially for the AI projects that are becoming increasingly popular these days.
The TDD Process
TDD follows a simple cycle as follows.
- Write a failing test
- Implement the minimum code to pass the test
- Refactor the code if necessary
This approach allows for writing robust applications. Although this might be a bit of overhead for very simple applications, when it comes to complex applications, TDD really makes your code clean, robust and saves a lot of time that you’d be otherwise spending debugging the code.
Let’s apply this process to our Todo application.
Prerequisites
If you are using the PyCharm IDE, the following steps can be easily completed in a single step, when creating a new project.
- A freshly created FastAPI project using PyCharm’s template
- Poetry installed and configured (refer to my detailed blog on using Poetry and pyproject.toml at https://medium.com/@rjsenanayaka/get-started-with-poetry-and-pyproject-toml-f36785bb0504)
After initializing the new Fast API project, open a terminal and run the following to add the required dependencies for the tests.
poetry add pytest httpx
Building the Todo Application
Initialize “tests” folder
To use poetry to run our tests easily, we need to create a folder named “tests” within the root of our project and create a file named “__init__.py”. This file can be empty, but it needs to be present within the “tests” folder.
Create the deliberately failing tests for “create” operation
Create a test file named tests/test_todos.py Inside here we write the unit tests for the “create” operation.
import pytest
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_create_todo():
response = client.post("/todos/", json={"title": "Test Todo", "description": "Test Description", "completed": False})
print(response.content) # Keep this for debugging if needed
assert response.status_code == 201
assert response.json()["title"] == "Test Todo"
assert response.json()["description"] == "Test Description"
assert response.json()["completed"] == False
assert "id" in response.json()
Run the command below to see that the above test case is failing (this is expected no worries!)
poetry run pytest
You will get an output similar to below
The error logs show that we are expecting a 201 (CREATED) status code, but get a 404 (NOT FOUND). This is expected because we haven’t written the code to create a todo entry (and thereby return 201 status code) yet!
Now, implement the relevant “create” operation in main.py
from fastapi import FastAPI
from models import Todo
app = FastAPI()
todos = []
@app.post("/todos/", status_code=201, response_model=Todo)
async def create_todo(todo: Todo):
todo.id = len(todos) + 1
todos.append(todo)
return todo
We also need to create a model class to easily handle the fields for each todo entry.
To do this, create a file named models.py
from pydantic import BaseModel, Field
class Todo(BaseModel):
id: int = Field(default=None)
title: str
description: str
completed: bool
Import this class in the main.py as below.
from models import Todo
Now again execute the command to run the tests.
poetry run pytest
Now the output should be something like below, the test passes!
Implement the other CRUD Operations
Now, let’s implement the remaining operations namely the read,update and delete using TDD.
Thus, we are first writing the tests instead of the actual business logic code. For simplicity, we are just maintaining a python list as our datastore instead of an actual database.
In the sections a,b,c below, we would be writing the test cases for each of the CRUD operations. Then for each of the operation we would write the business logic to pass each of the test cases.
After you write a test case in a file, you can run the tests you’ve written this far by executing the command
pytest test_todos.py
a) Read Operation
Add the following test to tests/test_todos.py
def test_read_todo():
# First, create a todo
create_response = client.post("/todos/", json={"title": "Read Todo", "description": "Todo to be read", "completed": False})
todo_id = create_response.json()["id"]
# Now, read the todo
read_response = client.get(f"/todos/{todo_id}")
assert read_response.status_code == 200
assert read_response.json()["title"] == "Read Todo"
assert read_response.json()["description"] == "Todo to be read"
assert read_response.json()["completed"] == False
assert read_response.json()["id"] == todo_id
Implement the “read” operation in main.py
@app.get("/todos/{todo_id}", response_model=Todo)
async def read_todo(todo_id: int):
for todo in todos:
if todo.id == todo_id:
return todo
raise HTTPException(status_code=404, detail="Todo not found")
b) Update Operation
Add the following test to tests/test_todos.py
def test_update_todo():
# First, create a todo
create_response = client.post("/todos/", json={"title": "Update Todo", "description": "Todo to be updated", "completed": False})
todo_id = create_response.json()["id"]
# Now, update the todo
update_response = client.put(f"/todos/{todo_id}", json={"title": "Updated Todo", "description": "Todo has been updated", "completed": True})
assert update_response.status_code == 200
assert update_response.json()["title"] == "Updated Todo"
assert update_response.json()["description"] == "Todo has been updated"
assert update_response.json()["completed"] == True
assert update_response.json()["id"] == todo_id
Implement the “update” operation in main.py
@app.put("/todos/{todo_id}", response_model=Todo)
async def update_todo(todo_id: int, updated_todo: Todo):
for index, todo in enumerate(todos):
if todo.id == todo_id:
updated_todo.id = todo_id
todos[index] = updated_todo
return updated_todo
raise HTTPException(status_code=404, detail="Todo not found")
c) Delete Operation
Add the following test to tests/test_todos.py
def test_delete_todo():
# First, create a todo
create_response = client.post("/todos/", json={"title": "Delete Todo", "description": "Todo to be deleted", "completed": False})
todo_id = create_response.json()["id"]
# Now, delete the todo
delete_response = client.delete(f"/todos/{todo_id}")
assert delete_response.status_code == 204
# Verify that the todo is deleted
read_response = client.get(f"/todos/{todo_id}")
assert read_response.status_code == 404
Implement the “delete” operation in main.py
@app.delete("/todos/{todo_id}", status_code=204)
async def delete_todo(todo_id: int):
for index, todo in enumerate(todos):
if todo.id == todo_id:
todos.pop(index)
return
raise HTTPException(status_code=404, detail="Todo not found")
Running the Tests
To run the tests, execute the following command (that we earlier ran as well) in your terminal.
You can also run specific test files using the below,
pytest test_todos.py
Or else we can also run all the test cases that you’ve written by executing the following command,
poetry run pytest
This will run all the four tests we’ve written, ensuring that our Todo application functions as expected.
Source code
You can find the source code for the project at the following link.
https://github.com/RukshanJS/youtube-tdd-fast-api
Conclusion
In this article, we’ve demonstrated the Test-Driven Development process by building a Todo application using FastAPI and pytest. We covered all the basic CRUD operations, writing tests first and then implementing the functionality to pass those tests.
TDD helps ensure that our code is working as expected from the start and makes it easier to refactor and maintain our codebase. By following this approach, you can build more robust and reliable applications.
Remember to continue this cycle of writing tests, implementing code, and refactoring as you add more features to your application. Happy coding!
Test-Driven Development (TDD) with Python: Building a FastAPI Todo Application was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Rukshan J. Senanayaka
Rukshan J. Senanayaka | Sciencx (2024-08-15T18:04:30+00:00) Test-Driven Development (TDD) with Python: Building a FastAPI Todo Application. Retrieved from https://www.scien.cx/2024/08/15/test-driven-development-tdd-with-python-building-a-fastapi-todo-application/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.