Skip to main content

FastAPI Testing Introduction

Testing is a crucial part of developing robust and reliable FastAPI applications. By implementing proper testing strategies, you can catch bugs early, ensure your API behaves as expected, and make changes confidently. In this guide, we'll explore how to set up and write tests for your FastAPI applications.

Why Test Your FastAPI Application?

Before diving into the technical details, let's understand why testing is important:

  • Quality Assurance: Tests help ensure your API works as intended
  • Regression Prevention: Tests catch bugs when you modify existing code
  • Documentation: Tests serve as living documentation for how your API should behave
  • Confidence: Tests give you confidence to refactor and add new features

Setting Up Testing Environment

To test FastAPI applications, we'll use pytest along with FastAPI's built-in testing tools.

Required Dependencies

First, install the necessary packages:

bash
pip install pytest httpx
  • pytest: The testing framework we'll use
  • httpx: An HTTP client that will help us make requests to our FastAPI application

Creating Your First FastAPI Test

Let's create a simple FastAPI application and write tests for it.

Sample FastAPI Application

Create a file named main.py:

python
from fastapi import FastAPI, HTTPException

app = FastAPI()

@app.get("/")
def read_root():
return {"message": "Hello World"}

@app.get("/items/{item_id}")
def read_item(item_id: int):
if item_id < 1:
raise HTTPException(status_code=400, detail="Item ID must be positive")
return {"item_id": item_id, "name": f"Item {item_id}"}

Writing Tests

Now, create a file named test_main.py:

python
from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_read_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}

def test_read_item():
response = client.get("/items/1")
assert response.status_code == 200
assert response.json() == {"item_id": 1, "name": "Item 1"}

def test_read_item_bad_id():
response = client.get("/items/-1")
assert response.status_code == 400
assert response.json() == {"detail": "Item ID must be positive"}

Running Tests

To run the tests, execute:

bash
pytest

You should see output similar to:

collected 3 items

test_main.py ... [100%]

================= 3 passed in 0.32s =================

Understanding TestClient

The TestClient class provided by FastAPI makes testing straightforward:

  • It wraps your FastAPI application
  • It simulates HTTP requests without actually starting a server
  • It returns responses that you can easily assert against

Testing Different HTTP Methods

Let's expand our API to include more HTTP methods and test them.

Extended API

Update main.py:

python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
name: str
price: float

items = {}

@app.get("/")
def read_root():
return {"message": "Hello World"}

@app.get("/items/{item_id}")
def read_item(item_id: int):
if item_id < 1:
raise HTTPException(status_code=400, detail="Item ID must be positive")
if item_id not in items:
raise HTTPException(status_code=404, detail="Item not found")
return items[item_id]

@app.post("/items/")
def create_item(item: Item):
item_id = len(items) + 1
items[item_id] = {"item_id": item_id, **item.dict()}
return items[item_id]

@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item):
if item_id not in items:
raise HTTPException(status_code=404, detail="Item not found")
items[item_id] = {"item_id": item_id, **item.dict()}
return items[item_id]

@app.delete("/items/{item_id}")
def delete_item(item_id: int):
if item_id not in items:
raise HTTPException(status_code=404, detail="Item not found")
deleted_item = items.pop(item_id)
return {"message": "Item deleted", "item": deleted_item}

Testing All HTTP Methods

Update test_main.py:

python
from fastapi.testclient import TestClient
from main import app, items

client = TestClient(app)

def test_read_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}

def test_read_nonexistent_item():
# Clear the items dictionary to ensure predictable state
items.clear()

response = client.get("/items/1")
assert response.status_code == 404
assert response.json() == {"detail": "Item not found"}

def test_create_item():
items.clear()

response = client.post(
"/items/",
json={"name": "Test Item", "price": 10.5}
)
assert response.status_code == 200
assert response.json() == {
"item_id": 1,
"name": "Test Item",
"price": 10.5
}

def test_read_item_after_creation():
# First create an item
client.post("/items/", json={"name": "Test Item", "price": 10.5})

# Then read it
response = client.get("/items/1")
assert response.status_code == 200
assert response.json() == {
"item_id": 1,
"name": "Test Item",
"price": 10.5
}

def test_update_item():
# First create an item
client.post("/items/", json={"name": "Test Item", "price": 10.5})

# Then update it
response = client.put(
"/items/1",
json={"name": "Updated Item", "price": 20.0}
)
assert response.status_code == 200
assert response.json() == {
"item_id": 1,
"name": "Updated Item",
"price": 20.0
}

def test_delete_item():
# First create an item
client.post("/items/", json={"name": "Test Item", "price": 10.5})

# Then delete it
response = client.delete("/items/1")
assert response.status_code == 200
assert "message" in response.json()
assert response.json()["message"] == "Item deleted"

# Verify it's gone
response = client.get("/items/1")
assert response.status_code == 404

Test Fixtures

To avoid repetitive setup code, we can use pytest fixtures:

python
import pytest
from fastapi.testclient import TestClient
from main import app, items

@pytest.fixture
def client():
# Clear items before each test
items.clear()
return TestClient(app)

def test_create_and_read_item(client):
# Create an item
create_response = client.post(
"/items/",
json={"name": "Fixture Item", "price": 15.75}
)
assert create_response.status_code == 200

# Read the created item
item_id = create_response.json()["item_id"]
read_response = client.get(f"/items/{item_id}")
assert read_response.status_code == 200
assert read_response.json()["name"] == "Fixture Item"

Testing Authentication

If your API uses authentication, you'll want to test both authenticated and unauthenticated requests.

Example API with Authentication

python
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

# Mock database of users
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"password": "secret",
"disabled": False,
}
}

def fake_decode_token(token):
return fake_users_db.get(token)

async def get_current_user(token: str = Depends(oauth2_scheme)):
user = fake_decode_token(token)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
)
return user

@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = fake_users_db.get(form_data.username)
if not user or form_data.password != user["password"]:
raise HTTPException(status_code=400, detail="Incorrect username or password")
return {"access_token": user["username"], "token_type": "bearer"}

@app.get("/users/me")
async def read_users_me(current_user = Depends(get_current_user)):
return current_user

Testing Authentication

python
from fastapi.testclient import TestClient
from auth_app import app

client = TestClient(app)

def test_login():
response = client.post(
"/token",
data={"username": "johndoe", "password": "secret"}
)
assert response.status_code == 200
assert "access_token" in response.json()

def test_read_users_me():
# First login to get a token
login_response = client.post(
"/token",
data={"username": "johndoe", "password": "secret"}
)
token = login_response.json()["access_token"]

# Use the token to access a protected endpoint
headers = {"Authorization": f"Bearer {token}"}
response = client.get("/users/me", headers=headers)

assert response.status_code == 200
assert response.json()["username"] == "johndoe"

def test_read_users_me_unauthorized():
# Try to access without a token
response = client.get("/users/me")
assert response.status_code == 401

Testing Best Practices

Here are some best practices to follow when testing FastAPI applications:

  1. Test Organization: Organize tests by endpoint or feature
  2. Use Fixtures: Reuse setup and teardown code with pytest fixtures
  3. Mock External Services: Use mocks for databases, APIs, etc.
  4. Test Edge Cases: Test with invalid inputs, boundary conditions, etc.
  5. Test Performance: Include tests for response times when relevant
  6. Parameterized Tests: Use pytest's parameterize to test multiple inputs

Summary

We've covered the basics of testing FastAPI applications:

  • Setting up the testing environment with pytest and TestClient
  • Writing tests for different HTTP methods
  • Using fixtures to reduce code duplication
  • Testing authenticated endpoints

Testing is a critical skill for building reliable FastAPI applications. By following the practices in this guide, you can ensure your API works correctly now and continues to work as your application grows.

Additional Resources

Exercises

  1. Write tests for a FastAPI endpoint that accepts query parameters
  2. Create tests for an endpoint that uploads files
  3. Implement tests for error handling in your API
  4. Set up a test database for integration testing
  5. Add performance tests for a computationally intensive endpoint

Happy testing!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)