FastAPI Test Client
Introduction
When building an API with FastAPI, it's crucial to ensure that your endpoints work as expected. Testing your API endpoints manually can be tedious and error-prone. Fortunately, FastAPI provides a built-in test client that allows you to simulate requests to your API and verify the responses programmatically.
The TestClient
class from FastAPI is based on HTTPX, a full-featured HTTP client for Python, which gives you the ability to make requests to your application without having to run an actual server. This makes testing faster, more reliable, and easier to integrate into your development workflow.
In this tutorial, we'll explore how to use the FastAPI TestClient
to test your API endpoints effectively.
Setting Up Your Testing Environment
Before we dive into using the TestClient
, let's set up our testing environment:
- First, make sure you have the required packages:
pip install fastapi pytest httpx
- Create a simple FastAPI application that we'll use for testing:
# main.py
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/")
async def read_root():
return {"message": "Hello World"}
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id < 0:
raise HTTPException(status_code=400, detail="Item ID cannot be negative")
return {"item_id": item_id}
@app.post("/items/")
async def create_item(name: str, price: float):
return {"name": name, "price": price, "id": 1}
Now we're ready to start testing our API using the TestClient
.
Basic Usage of TestClient
The TestClient
class allows you to make requests to your FastAPI application as if it were a real HTTP server. Here's how to use it:
# test_main.py
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"}
In this simple test:
- We import the
TestClient
fromfastapi.testclient
- We create a
client
instance by passing our FastAPI application to it - We use the
client
to make a GET request to the root endpoint/
- We assert that the response status code is 200 (OK)
- We assert that the response JSON matches our expected output
To run the test, execute:
pytest test_main.py -v
You should see output like:
test_main.py::test_read_root PASSED [100%]
Testing Different HTTP Methods
The TestClient
supports all standard HTTP methods. Let's add more tests for our other endpoints:
def test_read_item():
response = client.get("/items/42")
assert response.status_code == 200
assert response.json() == {"item_id": 42}
def test_read_item_bad_id():
response = client.get("/items/-1")
assert response.status_code == 400
assert response.json() == {"detail": "Item ID cannot be negative"}
def test_create_item():
response = client.post(
"/items/",
params={"name": "Test Item", "price": 10.5}
)
assert response.status_code == 200
assert response.json() == {"name": "Test Item", "price": 10.5, "id": 1}
Testing Request Bodies
For POST, PUT, and PATCH requests that require a request body, you can provide the data in different formats:
JSON Data
def test_create_item_json():
response = client.post(
"/items/",
json={"name": "Test Item", "price": 10.5}
)
assert response.status_code == 200
assert response.json() == {"name": "Test Item", "price": 10.5, "id": 1}
Form Data
def test_create_item_form():
response = client.post(
"/items/",
data={"name": "Test Item", "price": 10.5}
)
assert response.status_code == 200
assert response.json() == {"name": "Test Item", "price": 10.5, "id": 1}
Testing Headers and Cookies
You can also test endpoints that require specific headers or cookies:
def test_with_headers():
response = client.get(
"/",
headers={"X-Test": "test-value"}
)
assert response.status_code == 200
def test_with_cookies():
response = client.get(
"/",
cookies={"session": "test-cookie"}
)
assert response.status_code == 200
Real-World Example: Testing a User API
Let's look at a more complete example with a simple user management API:
# user_app.py
from fastapi import FastAPI, HTTPException, Depends, status
from pydantic import BaseModel
from typing import List, Optional
app = FastAPI()
class User(BaseModel):
id: Optional[int] = None
username: str
email: str
active: bool = True
# Mock database
users_db = {}
user_id_counter = 1
@app.post("/users/", response_model=User, status_code=status.HTTP_201_CREATED)
async def create_user(user: User):
global user_id_counter
user.id = user_id_counter
users_db[user_id_counter] = user
user_id_counter += 1
return user
@app.get("/users/", response_model=List[User])
async def get_users(skip: int = 0, limit: int = 10):
return list(users_db.values())[skip:skip+limit]
@app.get("/users/{user_id}", response_model=User)
async def get_user(user_id: int):
if user_id not in users_db:
raise HTTPException(status_code=404, detail="User not found")
return users_db[user_id]
@app.put("/users/{user_id}", response_model=User)
async def update_user(user_id: int, user: User):
if user_id not in users_db:
raise HTTPException(status_code=404, detail="User not found")
users_db[user_id].username = user.username
users_db[user_id].email = user.email
users_db[user_id].active = user.active
return users_db[user_id]
@app.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(user_id: int):
if user_id not in users_db:
raise HTTPException(status_code=404, detail="User not found")
del users_db[user_id]
return None
Now let's write tests for this API:
# test_user_app.py
from fastapi.testclient import TestClient
from user_app import app
client = TestClient(app)
def test_create_user():
response = client.post(
"/users/",
json={"username": "testuser", "email": "[email protected]"}
)
assert response.status_code == 201
data = response.json()
assert data["username"] == "testuser"
assert data["email"] == "[email protected]"
assert data["active"] == True
assert "id" in data
user_id = data["id"]
# Test that we can retrieve the created user
response = client.get(f"/users/{user_id}")
assert response.status_code == 200
assert response.json() == data
def test_create_and_get_users():
# Create first user
response = client.post(
"/users/",
json={"username": "user1", "email": "[email protected]"}
)
assert response.status_code == 201
# Create second user
response = client.post(
"/users/",
json={"username": "user2", "email": "[email protected]"}
)
assert response.status_code == 201
# Get all users
response = client.get("/users/")
assert response.status_code == 200
data = response.json()
assert len(data) >= 2
def test_update_user():
# Create a user first
response = client.post(
"/users/",
json={"username": "updateme", "email": "[email protected]"}
)
user_id = response.json()["id"]
# Update the user
updated_data = {
"username": "updated",
"email": "[email protected]",
"active": False
}
response = client.put(
f"/users/{user_id}",
json=updated_data
)
assert response.status_code == 200
data = response.json()
assert data["username"] == updated_data["username"]
assert data["email"] == updated_data["email"]
assert data["active"] == updated_data["active"]
def test_delete_user():
# Create a user first
response = client.post(
"/users/",
json={"username": "deleteme", "email": "[email protected]"}
)
user_id = response.json()["id"]
# Delete the user
response = client.delete(f"/users/{user_id}")
assert response.status_code == 204
# Verify the user is gone
response = client.get(f"/users/{user_id}")
assert response.status_code == 404
Advanced Testing Techniques
Testing with Dependencies
Often, your FastAPI application will have dependencies that you want to mock during testing. Here's how to handle that:
# app_with_dependencies.py
from fastapi import FastAPI, Depends, HTTPException
app = FastAPI()
async def get_db_connection():
# In a real app, this might connect to a database
return {"connected": True}
@app.get("/db-status/")
async def db_status(db=Depends(get_db_connection)):
return {"status": "connected" if db.get("connected") else "disconnected"}
To test this with a mock dependency:
# test_dependencies.py
from fastapi.testclient import TestClient
from fastapi import Depends
from app_with_dependencies import app, get_db_connection
client = TestClient(app)
# Override the dependency
async def mock_db_connection():
return {"connected": True, "mock": True}
app.dependency_overrides[get_db_connection] = mock_db_connection
def test_db_status():
response = client.get("/db-status/")
assert response.status_code == 200
assert response.json() == {"status": "connected"}
# Reset overrides after testing
app.dependency_overrides = {}
Testing with Authentication
If your API requires authentication, you can test it like this:
# auth_app.py
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def get_current_user(token: str = Depends(oauth2_scheme)):
if token != "valid-token":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return {"username": "testuser"}
@app.get("/users/me")
async def read_users_me(current_user: dict = Depends(get_current_user)):
return current_user
And here's how to test it:
# test_auth.py
from fastapi.testclient import TestClient
from auth_app import app, get_current_user
client = TestClient(app)
def test_read_users_me_unauthorized():
response = client.get("/users/me")
assert response.status_code == 401
def test_read_users_me():
response = client.get(
"/users/me",
headers={"Authorization": "Bearer valid-token"}
)
assert response.status_code == 200
assert response.json() == {"username": "testuser"}
Best Practices for Testing with TestClient
-
Keep tests isolated: Each test should be independent and not rely on the state from previous tests.
-
Use fixtures: For common setup and teardown operations, use pytest fixtures:
import pytest
from fastapi.testclient import TestClient
from user_app import app
@pytest.fixture
def client():
with TestClient(app) as client:
# Reset the database before each test
global users_db, user_id_counter
users_db = {}
user_id_counter = 1
yield client
def test_create_user(client):
response = client.post(
"/users/",
json={"username": "testuser", "email": "[email protected]"}
)
assert response.status_code == 201
-
Test both positive and negative scenarios: Make sure to test not only when things go right but also error conditions.
-
Use parameterized tests for testing similar behavior with different inputs:
@pytest.mark.parametrize(
"item_id,expected_status",
[
(1, 200),
(100, 200),
(-1, 400),
(0, 200),
],
)
def test_read_item_parametrized(client, item_id, expected_status):
response = client.get(f"/items/{item_id}")
assert response.status_code == expected_status
Summary
The FastAPI TestClient
is a powerful tool for testing your API endpoints without running a full server. It allows you to:
- Simulate HTTP requests to your application
- Test different HTTP methods (GET, POST, PUT, DELETE, etc.)
- Test with query parameters, request bodies, headers, and cookies
- Validate responses, status codes, and content
- Mock dependencies for more isolated testing
By using the TestClient
in combination with pytest, you can create a comprehensive test suite that ensures your API behaves as expected, handles errors correctly, and maintains compatibility as your application evolves.
Additional Resources
Exercises
-
Create a simple FastAPI application with CRUD operations for a "Todo" item and write tests for all endpoints.
-
Extend the user API example to include authentication with JWT tokens and write tests that verify protected endpoints.
-
Create an API that interacts with an external service and use dependency injection to mock the external service in your tests.
-
Write tests for an API that handles file uploads and downloads.
-
Create a parameterized test for an endpoint that should behave differently based on query parameters.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)