TestClient Basics

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.

Code examples

Basic TestClient Usage

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.

Testing POST and JSON Bodies

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.

Testing with Headers and Auth

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.

Key points

Concepts covered

TestClient, httpx, Assertions, Status Codes, Response Body