Difficulty: Advanced
Testing is essential for building reliable APIs. FastAPI provides a TestClient (built on httpx) that lets you send HTTP requests to your application without running an actual server. Tests run in-process, making them fast and deterministic. You can test endpoints, validate responses, check status codes, and verify error handling.
TestClient is imported from fastapi.testclient and takes your FastAPI app as an argument. It provides methods matching HTTP verbs: client.get(), client.post(), client.put(), client.delete(), etc. Each method returns a Response object with properties like status_code, json(), text, and headers.
A basic test creates a TestClient, makes a request, and asserts the response. For example, response = client.get("/") followed by assert response.status_code == 200 and assert response.json() == {"message": "hello"}. Tests use Python's assert statement or a testing framework like pytest.
TestClient can send all types of request data: query parameters (params={}), JSON bodies (json={}), form data (data={}), headers (headers={}), and files (files={}). This covers every type of request your API might receive.
For async endpoints, TestClient handles the event loop automatically. You do not need to use async test functions or special async test utilities - TestClient wraps everything synchronously. However, if you need to test with a real async client, httpx.AsyncClient with ASGITransport is available.
Tests should cover success cases, error cases, edge cases, and validation. Test that valid input returns the correct response, invalid input returns proper error codes, missing required fields return 422, and unauthorized requests return 401. Comprehensive tests give you confidence that your API works correctly and catch regressions early.
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/")
def root():
return {"message": "Hello, World!"}
@app.get("/items/{item_id}")
def get_item(item_id: int):
return {"item_id": item_id}
# Create test client
client = TestClient(app)
# Test the root endpoint
def test_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello, World!"}
# Test with path parameters
def test_get_item():
response = client.get("/items/42")
assert response.status_code == 200
assert response.json() == {"item_id": 42}
# Test validation error
def test_get_item_invalid():
response = client.get("/items/not-a-number")
assert response.status_code == 422
# Test 404
def test_not_found():
response = client.get("/nonexistent")
assert response.status_code == 404
TestClient sends requests to the app without starting a server. Each test function asserts the expected status code and response body. Invalid input tests verify that validation errors return 422.
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class ItemCreate(BaseModel):
name: str
price: float
items_db = {}
@app.post("/items", status_code=201)
def create_item(item: ItemCreate):
item_id = len(items_db) + 1
items_db[item_id] = {"id": item_id, item.model_dump()}
return items_db[item_id]
client = TestClient(app)
def test_create_item():
response = client.post(
"/items",
json={"name": "Widget", "price": 9.99},
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "Widget"
assert data["price"] == 9.99
assert "id" in data
def test_create_item_missing_field():
response = client.post(
"/items",
json={"name": "Widget"}, # Missing 'price'
)
assert response.status_code == 422
def test_create_item_wrong_type():
response = client.post(
"/items",
json={"name": "Widget", "price": "not-a-number"},
)
assert response.status_code == 422
Use json={} to send JSON request bodies. Test both valid and invalid inputs. Missing required fields and wrong types should return 422. The test verifies the response structure and values.
from fastapi import FastAPI, Depends, HTTPException, Header
from fastapi.testclient import TestClient
app = FastAPI()
def verify_token(authorization: str = Header()):
if authorization != "Bearer valid-token":
raise HTTPException(status_code=401, detail="Unauthorized")
return "authenticated-user"
@app.get("/protected")
def protected(user: str = Depends(verify_token)):
return {"user": user, "access": "granted"}
client = TestClient(app)
def test_protected_with_valid_token():
response = client.get(
"/protected",
headers={"Authorization": "Bearer valid-token"},
)
assert response.status_code == 200
assert response.json()["access"] == "granted"
def test_protected_without_token():
response = client.get("/protected")
assert response.status_code == 422 # Missing required header
def test_protected_with_invalid_token():
response = client.get(
"/protected",
headers={"Authorization": "Bearer wrong-token"},
)
assert response.status_code == 401
assert response.json()["detail"] == "Unauthorized"
Pass custom headers with headers={}. Test three authentication scenarios: valid token (200), missing token (422), and invalid token (401). Each test asserts both the status code and response body.
TestClient, httpx, Assertions, Status Codes, Response Body