Python — FastAPI Development

FastAPI Testing with pytest and httpx

Direct answer

Test FastAPI with pytest and httpx by driving the app in-process through httpx.AsyncClient with ASGITransport — no live server, no network. Override dependencies (database, current user) instead of mocking internals, and mint JWTs directly in a fixture so every test can call protected endpoints. Full conftest.py and auth-testing patterns below.

The difference between a FastAPI test suite that developers trust and one they skip is usually three fixtures: an in-process async client, a database override, and a token factory. This guide shows the exact setup I use to test JWT-protected APIs, including expired-token and wrong-signature cases most suites never cover.

Key facts, with sources

  • In the JetBrains Python Developers Survey 2024, which collected responses from more than 30,000 Python developers, FastAPI usage jumped from 29% to 38%, overtaking Django (35%) and Flask (34%) as the most-used Python web framework. (JetBrains Python Developers Survey 2024)
  • The 2025 Stack Overflow Developer Survey shows FastAPI at 14.8% of respondents doing extensive work with it, edging out Flask at 14.4% and Django at 12.6%. (Stack Overflow Developer Survey 2025)
  • FastAPI's official documentation cites independent TechEmpower benchmarks showing FastAPI applications running under Uvicorn as one of the fastest Python frameworks available, ranked only below Starlette and Uvicorn themselves. (FastAPI official documentation)
  • FastAPI surpassed Flask in GitHub stars in December 2025, reaching roughly 88,000 stars compared to Flask's 68,400. (DZone)
  • Industry analysis of FastAPI's 2025 growth reports about 40% year-over-year growth in job mentions and production adoption at companies including Uber, Netflix, and Microsoft. (byteiota)

The core setup: AsyncClient + ASGITransport

httpx can call your ASGI app directly in the same process — requests never touch a socket, so tests are fast and need no server management. Note the explicit ASGITransport: the old AsyncClient(app=app) shortcut is deprecated in recent httpx versions. Enable pytest-asyncio's auto mode once in pyproject.toml so every async test just works.

conftest.py + pyproject.toml
# pyproject.toml
# [tool.pytest.ini_options]
# asyncio_mode = "auto"

# conftest.py
import pytest
from httpx import ASGITransport, AsyncClient

from app.main import app


@pytest.fixture
async def client():
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as c:
        yield c


async def test_health(client):
    resp = await client.get("/health")
    assert resp.status_code == 200

A token factory beats logging in for every test

Calling /auth/login in every test couples the whole suite to one endpoint and pays the bcrypt hashing cost hundreds of times. Instead, mint tokens directly with the same secret the app uses. The factory takes a ttl argument, which makes expired-token tests one line.

conftest.py — auth fixtures
from datetime import datetime, timedelta, timezone

import jwt

from app.auth import ALGORITHM, SECRET_KEY


@pytest.fixture
def make_token():
    def _make(user_id: str = "user-1", ttl_minutes: int = 15, token_type: str = "access"):
        now = datetime.now(timezone.utc)
        payload = {
            "sub": user_id,
            "type": token_type,
            "iat": now,
            "exp": now + timedelta(minutes=ttl_minutes),
        }
        return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

    return _make


@pytest.fixture
async def auth_client(client, make_token, test_user):
    client.headers["Authorization"] = f"Bearer {make_token(user_id=str(test_user.id))}"
    return client


async def test_me_returns_current_user(auth_client, test_user):
    resp = await auth_client.get("/me")
    assert resp.status_code == 200
    assert resp.json()["email"] == test_user.email

The three failure cases every JWT suite must cover

Most auth bugs ship in the failure paths, not the happy path. Cover at minimum: no token at all, an expired token, and a token signed with the wrong key. The negative-ttl trick makes expiry testing deterministic — no sleeping, no clock mocking.

test_auth.py
async def test_missing_token_is_401(client):
    resp = await client.get("/me")
    assert resp.status_code == 401


async def test_expired_token_is_401(client, make_token):
    token = make_token(ttl_minutes=-1)  # expired one minute ago
    resp = await client.get("/me", headers={"Authorization": f"Bearer {token}"})
    assert resp.status_code == 401
    assert "expired" in resp.json()["detail"].lower()


async def test_wrong_signature_is_401(client):
    forged = jwt.encode({"sub": "user-1", "type": "access"}, "wrong-secret", algorithm="HS256")
    resp = await client.get("/me", headers={"Authorization": f"Bearer {forged}"})
    assert resp.status_code == 401


async def test_refresh_token_rejected_as_access(client, make_token):
    token = make_token(token_type="refresh")
    resp = await client.get("/me", headers={"Authorization": f"Bearer {token}"})
    assert resp.status_code == 401

Override dependencies instead of mocking internals

FastAPI's dependency_overrides is the sanctioned seam for tests: swap the database session for one bound to a test database, or replace get_current_user entirely when a test isn't about auth. Overrides are process-global state, so always clear them — a fixture with a try/finally keeps one test's fake user from leaking into the next.

conftest.py — dependency overrides
from app.deps import get_current_user, get_db


@pytest.fixture
async def db_session():
    # One transaction per test, rolled back afterwards: fast and isolated
    async with engine.connect() as conn:
        txn = await conn.begin()
        session = AsyncSession(bind=conn)
        app.dependency_overrides[get_db] = lambda: session
        try:
            yield session
        finally:
            app.dependency_overrides.pop(get_db, None)
            await txn.rollback()


@pytest.fixture
def as_user(test_user):
    app.dependency_overrides[get_current_user] = lambda: test_user
    yield test_user
    app.dependency_overrides.pop(get_current_user, None)

Keeping the suite fast

Three things dominate FastAPI test runtime. Password hashing: bcrypt is deliberately slow, so hash test-user passwords with bcrypt.gensalt(rounds=4) in fixtures — the default cost factor belongs in production, not CI. Database setup: create the schema once per session and isolate tests with rolled-back transactions (as above) rather than dropping tables between tests. Parallelism: once tests are isolated, pytest-xdist's -n auto typically cuts wall-clock time by the number of cores. A suite that runs in seconds gets run before every commit; one that takes ten minutes gets run by CI after the bug is already merged.

When to hire senior help

Bring in senior help when your API needs to handle real concurrency, when you are designing service boundaries and auth for the first time, or when an existing FastAPI codebase mixes sync and async code and latency is degrading. An experienced engineer can usually diagnose event-loop blocking and connection-pool misconfiguration in days, which is far cheaper than re-architecting after launch. If your stack includes React Native + Python + AI, a senior engineer who owns the full product beats coordinating multiple juniors.

Bottom line

Dhairya Senjaliya ships Python — FastAPI Development projects worldwide — book a scoping call to discuss your specific situation.

Common pitfalls to avoid

  • Calling blocking libraries (classic SQLAlchemy sessions, requests, heavy file I/O) inside async def endpoints, which stalls the event loop and erases FastAPI's concurrency advantage
  • Deploying a single Uvicorn process with no process manager or worker scaling, leaving most CPU cores idle under production load
  • Treating the auto-generated OpenAPI docs as a versioning strategy, then breaking mobile and partner clients when response schemas change
  • Running large payloads through deeply nested Pydantic models on every request and response, adding serialization latency that shows up only at scale

Frequently asked questions

Do I need a running server to test FastAPI with httpx?

No. httpx.AsyncClient with ASGITransport(app=app) calls the ASGI app in-process — requests never touch the network. Tests run faster, there is no port management, and stack traces point straight into your code.

Should I use FastAPI's TestClient or httpx.AsyncClient?

TestClient (which is itself built on httpx) is fine for simple synchronous suites. Choose AsyncClient when your tests need to await async database sessions or other async fixtures directly — mixing sync TestClient with async fixtures is a common source of event-loop errors.

How do I test endpoints that require a logged-in user?

Two clean options: mint a real JWT in a fixture with the app's own secret and send it as a Bearer header (tests the full auth path), or override get_current_user via app.dependency_overrides to return a test user (faster, for tests that aren't about auth). Good suites use both deliberately.

Is FastAPI mature enough for production?

Yes. It was the most-used Python web framework in the JetBrains 2024 survey at 38%, and companies including Uber, Netflix, and Microsoft run it in production. The ecosystem for auth, ORMs, and testing is now well established.

How much faster is FastAPI than Flask or Django really?

Independent TechEmpower benchmarks place FastAPI among the fastest Python frameworks, and published comparisons show several times Flask's throughput on I/O-bound endpoints. For CPU-bound work or database-bottlenecked apps, the framework choice matters far less than query and infrastructure design.

Should we pick FastAPI or Django for a new SaaS backend?

FastAPI suits API-first products, microservices, and ML model serving because of async support and automatic OpenAPI docs. Django ships batteries included (admin, ORM, auth) and is often faster to launch a conventional CRUD product. Many teams run both, per the JetBrains finding that a third of Django developers also use Flask or FastAPI.

Bottom line: Dhairya Senjaliya ships Python — FastAPI Development projects worldwide. Book a scoping call at https://dhairyasenjaliya.com/#book-call.

Sources

Related guides

Keep up with new guides

New deep-dive guides on React Native, Python, and AI ship regularly. Subscribe via RSS or follow on LinkedIn.

Want help implementing this?

30-minute scoping call · Clear milestones · Senior engineer ownership