Python 3 min read

FastAPI Tutorial Part 15: Testing Your API

Write comprehensive tests for FastAPI applications. Learn pytest, TestClient, database testing, mocking, and test-driven development patterns.

MR

Moshiour Rahman

Advertisement

Setup

pip install pytest pytest-asyncio httpx

Basic Testing with TestClient

# tests/test_main.py
from fastapi.testclient import TestClient
from app.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_create_item():
    response = client.post(
        "/items",
        json={"name": "Test Item", "price": 9.99}
    )
    assert response.status_code == 201
    assert response.json()["name"] == "Test Item"

def test_get_item_not_found():
    response = client.get("/items/999")
    assert response.status_code == 404

Async Testing

import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app

@pytest.mark.asyncio
async def test_async_endpoint():
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.get("/async-endpoint")
        assert response.status_code == 200

Database Testing

# tests/conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.database import Base, get_db
from app.main import app

SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
TestingSessionLocal = sessionmaker(bind=engine)

@pytest.fixture
def db_session():
    Base.metadata.create_all(bind=engine)
    session = TestingSessionLocal()
    try:
        yield session
    finally:
        session.close()
        Base.metadata.drop_all(bind=engine)

@pytest.fixture
def client(db_session):
    def override_get_db():
        yield db_session

    app.dependency_overrides[get_db] = override_get_db
    yield TestClient(app)
    app.dependency_overrides.clear()

Testing Authentication

@pytest.fixture
def auth_headers(client):
    # Register user
    client.post("/auth/register", json={
        "email": "test@test.com",
        "username": "testuser",
        "password": "testpass123"
    })

    # Login
    response = client.post("/auth/login", data={
        "username": "testuser",
        "password": "testpass123"
    })
    token = response.json()["access_token"]
    return {"Authorization": f"Bearer {token}"}

def test_protected_endpoint(client, auth_headers):
    response = client.get("/users/me", headers=auth_headers)
    assert response.status_code == 200
    assert response.json()["username"] == "testuser"

def test_unauthorized_access(client):
    response = client.get("/users/me")
    assert response.status_code == 401

Mocking

from unittest.mock import patch, MagicMock

def test_external_api_call(client):
    with patch("app.services.external_api.fetch_data") as mock:
        mock.return_value = {"data": "mocked"}
        response = client.get("/external-data")
        assert response.json()["data"] == "mocked"

Complete Test Suite

# tests/test_items.py
import pytest
from fastapi.testclient import TestClient
from app.main import app

class TestItems:
    @pytest.fixture(autouse=True)
    def setup(self, client):
        self.client = client

    def test_create_item(self):
        response = self.client.post("/items", json={
            "name": "Widget",
            "price": 29.99,
            "category": "electronics"
        })
        assert response.status_code == 201
        data = response.json()
        assert data["name"] == "Widget"
        assert "id" in data

    def test_list_items(self):
        # Create items
        for i in range(3):
            self.client.post("/items", json={
                "name": f"Item {i}",
                "price": 10.0 + i,
                "category": "test"
            })

        response = self.client.get("/items")
        assert response.status_code == 200
        assert len(response.json()["items"]) >= 3

    def test_filter_items(self):
        response = self.client.get("/items?min_price=20&max_price=50")
        assert response.status_code == 200

    def test_validation_error(self):
        response = self.client.post("/items", json={
            "name": "",  # Invalid
            "price": -10  # Invalid
        })
        assert response.status_code == 422

Running Tests

# Run all tests
pytest

# With coverage
pytest --cov=app --cov-report=html

# Specific test file
pytest tests/test_items.py -v

# Run specific test
pytest tests/test_items.py::TestItems::test_create_item -v

Summary

ToolPurpose
TestClientSync testing
AsyncClientAsync testing
pytest.fixtureSetup/teardown
mock.patchMocking external calls

Next Steps

In Part 16, we’ll explore Docker and Deployment - containerizing and deploying FastAPI applications.

Series Navigation:

  • Part 1-14: Previous parts
  • Part 15: Testing (You are here)
  • Part 16: Docker & Deployment

Advertisement

MR

Moshiour Rahman

Software Architect & AI Engineer

Share:
MR

Moshiour Rahman

Software Architect & AI Engineer

Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.

Related Articles

Comments

Comments are powered by GitHub Discussions.

Configure Giscus at giscus.app to enable comments.