All posts
·4 min read·0 views·Shashank Bindal

Testing FastAPI with Quelltest: What Works, What Doesn't, and Why

A practical guide to using Quelltest on FastAPI projects — including which parts of your codebase it scans and which it skips.

FastAPI is one of the most popular Python web frameworks — and also one of the most Quelltest-friendly, because FastAPI codebases are full of exactly the kind of specs Quelltest can read: Pydantic models with Field validators, docstrings with Raises: sections, and explicit return types.

This guide covers where Quelltest adds value on a FastAPI project and where you need to do things manually (for now).

What Quelltest reads in a FastAPI project

Pydantic request/response models

FastAPI relies heavily on Pydantic models. Every Field constraint in a request or response model is a testable requirement:

from pydantic import BaseModel, Field
from typing import Literal

class PaymentRequest(BaseModel):
    amount: float = Field(gt=0, le=10_000)
    currency: Literal["USD", "EUR", "GBP", "INR"]
    idempotency_key: str = Field(min_length=8, max_length=64)

Running quell check on this produces three requirements:

  • BOUNDARY: amount must be > 0 and ≤ 10,000
  • ENUM_VALID: currency must be one of USD, EUR, GBP, INR
  • BOUNDARY: idempotency_key length must be between 8 and 64

Each one gets a verified test that passes on valid inputs and fails when the constraint is violated.

Service layer docstrings

FastAPI route handlers are async — Quelltest skips them (see 0.6.9 release notes). But the service layer under them is typically synchronous and often well-documented:

def process_payment(request: PaymentRequest) -> PaymentResponse:
    """
    Process a payment request.

    Args:
        request: Validated payment request.

    Returns:
        PaymentResponse with a unique transaction_id.

    Raises:
        InsufficientFundsError: If the account balance is below request.amount.
        DuplicateTransactionError: If idempotency_key was seen in the last 24h.
    """
    ...

Quelltest extracts two MUST_RAISE requirements from this docstring. It then generates tests that:

  1. Call process_payment with conditions that trigger each exception.
  2. Run both tests on the original code (must pass) and on code where the raise is commented out (must fail).

What Quelltest skips

Async route handlers

@app.post("/payments")
async def create_payment(request: PaymentRequest) -> PaymentResponse:
    return await payment_service.process(request)

Route handlers like this are skipped entirely. The reason: generating pytest tests for async functions requires pytest-asyncio setup, and the violation injection mechanism does not yet handle coroutines safely. Support for async scanning is planned for 0.7.x.

Dependency injection

FastAPI's Depends() pattern adds implicit arguments that Quelltest cannot see at AST time. Functions that take db: Session = Depends(get_db) are scanned for requirements in their docstrings, but the generated tests will not know how to construct the dependency graph — they'll need manual fixture setup.

Recommended project layout

To get the most out of Quelltest on a FastAPI project, keep business logic in a separate service layer:

app/
├── routes/          ← async handlers, Quelltest skips these
│   └── payments.py
├── services/        ← synchronous service functions, Quelltest scans here
│   └── payment_service.py
├── models/          ← Pydantic models, Quelltest scans here
│   └── payment.py
└── schemas/         ← PySpark schemas if applicable
    └── transaction_schema.py

Then target the non-route directories:

quell check app/services/ app/models/ --fix --no-llm

Running for the first time

pip install quelltest
quell check app/services/ app/models/ --no-llm

The first scan will show your current requirement coverage score. If you have existing tests, many requirements may already be covered. --fix only generates tests for gaps.

CI integration

Add to your GitHub Actions workflow:

- name: Quelltest requirement coverage
  run: |
    pip install quelltest
    quell check app/services/ app/models/ --ci --threshold 0.75

This fails the build if requirement coverage drops below 75%. Adjust the threshold based on your team's quality bar.

What a verified test looks like

For the process_payment docstring above, Quelltest would generate something like:

def test_process_payment_raises_insufficient_funds():
    """MUST_RAISE InsufficientFundsError — auto-generated by Quelltest 0.6.9."""
    from app.services.payment_service import process_payment
    from app.models.payment import PaymentRequest
    import pytest

    request = PaymentRequest(amount=0.01, currency="USD", idempotency_key="test-key-1")
    with pytest.raises(InsufficientFundsError):
        process_payment(request)

This test is verified: it passes on the original process_payment implementation and fails when the raise InsufficientFundsError line is commented out. Only then is it written to your test file.

Summary

SourceScanned?Notes
Pydantic request/response modelsField validators, Literal types
Service layer docstringsRaises:, Returns:, Args:
Async route handlersSkipped; planned for 0.7.x
FastAPI Depends()Cannot resolve at AST time
PySpark schemasStructType, StructField

Try Quelltest

Install Quelltest and run it on your codebase — no API key, no configuration.