FastAPI Testing API with Pytest

Untested code is broken code you have not discovered yet. FastAPI includes a test client that sends real HTTP requests to your app without starting a server. You write test functions with pytest, and they run your entire API stack — routing, validation, dependencies — in memory.

Install Testing Tools

pip install pytest httpx

HTTPx powers the TestClient. Pytest is the test runner that finds and executes test functions.

Your First Test

# main.py
from fastapi import FastAPI
app = FastAPI()

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

# test_main.py
from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_hello():
    response = client.get("/hello")
    assert response.status_code == 200
    assert response.json() == {"message": "Hello"}
Run tests:
  pytest test_main.py

Output:
  test_main.py::test_hello PASSED    [100%]

Testing POST with a Body

@app.post("/users", status_code=201)
def create_user(name: str, email: str):
    return {"id": 1, "name": name, "email": email}

def test_create_user():
    response = client.post("/users", json={"name": "Meera", "email": "m@example.com"})
    assert response.status_code == 201
    data = response.json()
    assert data["name"] == "Meera"
    assert "id" in data

Testing Error Cases

@app.get("/users/{user_id}")
def get_user(user_id: int):
    if user_id != 1:
        raise HTTPException(404, detail="Not found")
    return {"id": 1}

def test_user_not_found():
    response = client.get("/users/999")
    assert response.status_code == 404
    assert response.json()["detail"] == "Not found"

def test_invalid_id_type():
    response = client.get("/users/abc")
    assert response.status_code == 422   ← validation error

Fixtures — Shared Setup for Multiple Tests

Pytest fixtures create reusable setup code that runs before each test:

import pytest
from fastapi.testclient import TestClient
from main import app

@pytest.fixture
def client():
    return TestClient(app)

def test_hello(client):
    response = client.get("/hello")
    assert response.status_code == 200

def test_create_user(client):
    response = client.post("/users", json={"name": "Raj"})
    assert response.status_code == 201

Overriding the Database Dependency for Tests

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from database import Base, get_db
from main import app

TEST_DB_URL = "sqlite:///./test.db"
engine = create_engine(TEST_DB_URL, connect_args={"check_same_thread": False})
TestSessionLocal = sessionmaker(bind=engine)

@pytest.fixture(autouse=True)
def setup_db():
    Base.metadata.create_all(bind=engine)   ← create tables
    yield
    Base.metadata.drop_all(bind=engine)     ← drop tables after test

def override_get_db():
    db = TestSessionLocal()
    try:
        yield db
    finally:
        db.close()

app.dependency_overrides[get_db] = override_get_db
Each test:
  gets a fresh test database
  runs against it
  database dropped clean afterward
  next test starts with an empty database

Testing with Auth Headers

def test_protected_route():
    token = "valid-jwt-token-here"
    response = client.get(
        "/me",
        headers={"Authorization": f"Bearer {token}"}
    )
    assert response.status_code == 200

def test_protected_without_token():
    response = client.get("/me")   ← no auth header
    assert response.status_code == 401

Test Naming and Organization

project/
├── main.py
├── routers/
│   ├── users.py
│   └── products.py
└── tests/
    ├── __init__.py
    ├── test_users.py
    └── test_products.py

Naming rules:
  File:     test_*.py or *_test.py
  Function: def test_*():
  Class:    class Test*:

Key Points

  • Use TestClient(app) from FastAPI to send requests without starting a real server.
  • Assert on response.status_code and response.json() to verify behavior.
  • Use pytest fixtures to share setup and teardown logic across multiple tests.
  • Override dependencies with app.dependency_overrides to swap the real database for a test one.
  • Test both success and failure paths — missing fields, wrong types, not-found errors.

Leave a Comment

Your email address will not be published. Required fields are marked *