Back to Blog
Article

Best Tools for Testing FastAPI Applications in 2026 | pytest, HTTPX, Locust & More

By ForgeAPI Team

Testing is non-negotiable for production-ready APIs. FastAPI's design makes testing straightforward, and the Python ecosystem provides powerful tools to ensure your API works flawlessly. This guide covers the essential testing toolkit every FastAPI developer needs.


Core Testing Framework: pytest

pytest is the de facto testing framework for Python and FastAPI applications.

Installation

pip install pytest pytest-asyncio pytest-cov

Basic Test Structure

# tests/conftest.py
import pytest
from httpx import AsyncClient, ASGITransport
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
 
from app.main import app
from app.database import Base, get_db
 
# Create test database engine
TEST_DATABASE_URL = "sqlite+aiosqlite:///./test.db"
engine = create_async_engine(TEST_DATABASE_URL)
TestingSessionLocal = sessionmaker(engine, class_=AsyncSession)
 
@pytest.fixture(scope="session")
def event_loop():
    import asyncio
    loop = asyncio.get_event_loop_policy().new_event_loop()
    yield loop
    loop.close()
 
@pytest.fixture
async def db_session():
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    
    async with TestingSessionLocal() as session:
        yield session
    
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)
 
@pytest.fixture
async def client(db_session):
    async def override_get_db():
        yield db_session
    
    app.dependency_overrides[get_db] = override_get_db
    
    async with AsyncClient(
        transport=ASGITransport(app=app),
        base_url="http://test"
    ) as ac:
        yield ac
    
    app.dependency_overrides.clear()

HTTP Testing: HTTPX and TestClient

HTTPX for Async Testing (Recommended)

# tests/test_users.py
import pytest
from httpx import AsyncClient
 
@pytest.mark.asyncio
async def test_create_user(client: AsyncClient):
    response = await client.post(
        "/api/v1/users",
        json={
            "email": "test@example.com",
            "password": "securepassword123",
            "name": "Test User"
        }
    )
    assert response.status_code == 201
    data = response.json()
    assert data["email"] == "test@example.com"
    assert "id" in data
    assert "password" not in data  # Ensure password is not exposed
 
@pytest.mark.asyncio
async def test_get_user(client: AsyncClient, created_user):
    response = await client.get(f"/api/v1/users/{created_user['id']}")
    assert response.status_code == 200
    assert response.json()["email"] == created_user["email"]
 
@pytest.mark.asyncio
async def test_create_user_invalid_email(client: AsyncClient):
    response = await client.post(
        "/api/v1/users",
        json={"email": "invalid-email", "password": "test"}
    )
    assert response.status_code == 422
    assert "email" in response.json()["detail"][0]["loc"]

FastAPI TestClient (Sync Alternative)

from fastapi.testclient import TestClient
from app.main import app
 
client = TestClient(app)
 
def test_read_main():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "Hello World"}

Mocking and Patching

unittest.mock for Dependencies

from unittest.mock import AsyncMock, patch
import pytest
 
@pytest.mark.asyncio
async def test_payment_processing(client: AsyncClient):
    # Mock external payment service
    with patch("app.services.payment.stripe_client") as mock_stripe:
        mock_stripe.create_charge = AsyncMock(return_value={
            "id": "ch_123",
            "status": "succeeded"
        })
        
        response = await client.post(
            "/api/v1/payments",
            json={"amount": 1000, "currency": "usd"}
        )
        
        assert response.status_code == 200
        mock_stripe.create_charge.assert_called_once()

pytest-mock for Cleaner Mocking

pip install pytest-mock
@pytest.mark.asyncio
async def test_send_notification(client: AsyncClient, mocker):
    mock_send = mocker.patch(
        "app.services.email.send_email",
        new_callable=AsyncMock
    )
    
    response = await client.post(
        "/api/v1/notifications",
        json={"user_id": 1, "message": "Hello"}
    )
    
    assert response.status_code == 200
    mock_send.assert_called_once_with(
        to="user@example.com",
        subject=mocker.ANY,
        body=mocker.ANY
    )

Respx for HTTP Mocking

pip install respx
import respx
from httpx import Response
 
@pytest.mark.asyncio
async def test_external_api_call(client: AsyncClient):
    with respx.mock:
        respx.get("https://api.external.com/data").mock(
            return_value=Response(200, json={"status": "ok"})
        )
        
        response = await client.get("/api/v1/fetch-external")
        assert response.status_code == 200
        assert response.json()["external_status"] == "ok"

Database Testing

Factory Boy for Test Data

pip install factory-boy
# tests/factories.py
import factory
from factory.alchemy import SQLAlchemyModelFactory
from app.models import User, Post
 
class UserFactory(SQLAlchemyModelFactory):
    class Meta:
        model = User
        sqlalchemy_session_persistence = "commit"
    
    email = factory.Sequence(lambda n: f"user{n}@example.com")
    name = factory.Faker("name")
    hashed_password = "hashedpassword123"
    is_active = True
 
class PostFactory(SQLAlchemyModelFactory):
    class Meta:
        model = Post
    
    title = factory.Faker("sentence")
    content = factory.Faker("paragraph")
    author = factory.SubFactory(UserFactory)
# tests/test_posts.py
@pytest.mark.asyncio
async def test_list_user_posts(client: AsyncClient, db_session):
    user = UserFactory(session=db_session)
    PostFactory.create_batch(5, author=user, session=db_session)
    
    response = await client.get(f"/api/v1/users/{user.id}/posts")
    
    assert response.status_code == 200
    assert len(response.json()) == 5

Faker for Realistic Data

from faker import Faker
 
fake = Faker()
 
@pytest.mark.asyncio
async def test_user_profile_update(client: AsyncClient, auth_token):
    new_data = {
        "name": fake.name(),
        "bio": fake.text(max_nb_chars=200),
        "location": fake.city()
    }
    
    response = await client.patch(
        "/api/v1/profile",
        json=new_data,
        headers={"Authorization": f"Bearer {auth_token}"}
    )
    
    assert response.status_code == 200
    assert response.json()["name"] == new_data["name"]

API Contract Testing

Schemathesis for Property-Based Testing

pip install schemathesis
# tests/test_schema.py
import schemathesis
from app.main import app
 
schema = schemathesis.from_asgi("/openapi.json", app)
 
@schema.parametrize()
def test_api_contracts(case):
    """Tests all endpoints against their OpenAPI schema."""
    response = case.call_asgi()
    case.validate_response(response)

Run from CLI:

schemathesis run http://localhost:8000/openapi.json

Hypothesis for Data Generation

pip install hypothesis
from hypothesis import given, strategies as st
 
@given(
    name=st.text(min_size=1, max_size=100),
    age=st.integers(min_value=18, max_value=120)
)
@pytest.mark.asyncio
async def test_user_creation_properties(client: AsyncClient, name, age):
    response = await client.post(
        "/api/v1/users",
        json={"name": name, "age": age, "email": f"{name}@test.com"}
    )
    
    if response.status_code == 201:
        data = response.json()
        assert data["name"] == name
        assert data["age"] == age

Performance Testing

Locust for Load Testing

pip install locust
# locustfile.py
from locust import HttpUser, task, between
 
class FastAPIUser(HttpUser):
    wait_time = between(1, 3)
    
    def on_start(self):
        # Login and store token
        response = self.client.post("/auth/login", json={
            "email": "test@example.com",
            "password": "password123"
        })
        self.token = response.json()["access_token"]
        self.headers = {"Authorization": f"Bearer {self.token}"}
    
    @task(3)
    def list_items(self):
        self.client.get("/api/v1/items", headers=self.headers)
    
    @task(1)
    def create_item(self):
        self.client.post(
            "/api/v1/items",
            json={"name": "Test Item", "price": 9.99},
            headers=self.headers
        )
    
    @task(2)
    def get_item(self):
        self.client.get("/api/v1/items/1", headers=self.headers)
locust -f locustfile.py --host=http://localhost:8000

pytest-benchmark for Micro-Benchmarks

pip install pytest-benchmark
def test_serialization_performance(benchmark):
    from app.schemas import UserResponse
    from app.models import User
    
    user = User(id=1, email="test@example.com", name="Test")
    
    def serialize():
        return UserResponse.model_validate(user)
    
    result = benchmark(serialize)
    assert result.email == "test@example.com"

Code Coverage

pytest-cov

pip install pytest-cov
# Run tests with coverage
pytest --cov=app --cov-report=html --cov-report=term-missing
 
# Set minimum coverage threshold
pytest --cov=app --cov-fail-under=80
# pyproject.toml
[tool.coverage.run]
source = ["app"]
omit = ["app/tests/*", "app/migrations/*"]
 
[tool.coverage.report]
exclude_lines = [
    "pragma: no cover",
    "def __repr__",
    "raise NotImplementedError",
    "if TYPE_CHECKING:",
]

End-to-End Testing

Playwright for Browser Testing

pip install playwright pytest-playwright
playwright install
# tests/e2e/test_api_docs.py
import pytest
from playwright.async_api import Page
 
@pytest.mark.asyncio
async def test_swagger_ui_loads(page: Page):
    await page.goto("http://localhost:8000/docs")
    
    # Wait for Swagger UI to load
    await page.wait_for_selector(".swagger-ui")
    
    # Check title
    title = await page.text_content(".title")
    assert "FastAPI" in title
    
    # Test endpoint expansion
    await page.click("text=/api/v1/users")
    await page.wait_for_selector(".responses-wrapper")

Testing WebSockets

import pytest
from httpx_ws import aconnect_ws
 
@pytest.mark.asyncio
async def test_websocket_chat(client: AsyncClient):
    async with aconnect_ws(
        "http://test/ws/chat/room1",
        client
    ) as ws:
        # Send message
        await ws.send_json({"message": "Hello"})
        
        # Receive echo
        response = await ws.receive_json()
        assert response["message"] == "Hello"

Complete Testing Configuration

pyproject.toml

[tool.pytest.ini_options]
minversion = "7.0"
asyncio_mode = "auto"
testpaths = ["tests"]
python_files = ["test_*.py"]
python_functions = ["test_*"]
addopts = [
    "-v",
    "--tb=short",
    "--strict-markers",
    "-ra",
]
markers = [
    "slow: marks tests as slow",
    "integration: marks tests as integration tests",
    "e2e: marks tests as end-to-end tests",
]
 
[tool.coverage.run]
source = ["app"]
branch = true
 
[tool.coverage.report]
fail_under = 80
show_missing = true

Testing Best Practices Checklist

Use async tests from day one — Avoid event loop issues later
Isolate database state — Each test gets a clean slate
Mock external services — Tests shouldn't depend on third parties
Test edge cases — Empty inputs, large payloads, concurrent requests
Measure coverage — Aim for 80%+ on critical paths
Run tests in CI — Automate on every pull request
Use factories — Don't duplicate test data setup
Test error responses — Verify proper error handling


Conclusion

A comprehensive testing strategy is essential for building reliable FastAPI applications. Start with pytest and HTTPX for core testing, add mocking for external dependencies, integrate property-based testing for edge cases, and implement load testing before production deployments.

ForgeAPI includes a complete testing setup with fixtures, factories, and CI integration out of the box, so you can focus on writing tests instead of configuring tools.


Related Resources: