How to Test FastCRUD Endpoints: A Complete Guide with Examples

Test FastCRUD endpoints by creating a temporary FastAPI application with the generated crud_router, injecting an async test session dependency, and using FastAPI's TestClient to execute HTTP requests against the auto-generated CRUD routes.

FastCRUD accelerates API development by automatically generating REST endpoints from SQLModel or SQLAlchemy models. Because the router is constructed dynamically at runtime in fastcrud/endpoint/crud_router.py, testing requires bootstrapping a full FastAPI application within your test suite. This guide demonstrates the exact patterns used in the benavlabs/fastcrud repository to validate endpoint behavior, including dependency injection, soft deletes, and advanced query parameters.

Understanding FastCRUD's Endpoint Architecture

Before writing tests, you must understand how FastCRUD constructs its API layer. The library generates routes through two primary components:

When you call crud_router(), FastCRUD inspects your model and creates standard REST endpoints (POST /create, GET /get/{id}, GET /get_multi, PATCH /update/{id}, DELETE /delete/{id}) with built-in support for pagination, filtering, and sorting.

Setting Up the Test Environment

The repository provides a reference implementation for test infrastructure in tests/sqlmodel/conftest.py. Your test suite requires three core components:

  1. An async database engine (typically SQLite with aiosqlite for in-memory testing)
  2. A session fixture that yields AsyncSession instances
  3. A FastAPI TestClient that wraps your temporary app

This pattern ensures each test runs against an isolated, fresh database schema while maintaining full compatibility with FastCRUD's async repository pattern.

Bootstrapping a Temporary FastAPI Application

Because FastCRUD routers are generated at runtime, you cannot import them as static modules in tests. Instead, instantiate the router inside a fixture and mount it to a fresh FastAPI application.

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from sqlmodel import SQLModel
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from typing import AsyncGenerator

# Import FastCRUD components

from fastcrud.endpoint.crud_router import crud_router
from your_app.models import Item
from your_app.schemas import CreateItemSchema, UpdateItemSchema

@pytest.fixture
async def async_session():
    """Create a new async session for each test."""
    engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False)
    async with engine.begin() as conn:
        await conn.run_sync(SQLModel.metadata.create_all)
    
    session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
    async with session_factory() as session:
        yield session
    await engine.dispose()

@pytest.fixture
def client(async_session):
    """Build a FastAPI app with the CRUD router and return a TestClient."""
    
    async def lifespan(app: FastAPI):
        # Tables already created in session fixture

        yield
    
    app = FastAPI(lifespan=lifespan)
    
    # Dependency injection for the test session

    async def get_session() -> AsyncGenerator[AsyncSession, None]:
        yield async_session
    
    # Generate the CRUD router at runtime

    item_router = crud_router(
        session=get_session,
        model=Item,
        create_schema=CreateItemSchema,
        update_schema=UpdateItemSchema,
        path="/items",
        tags=["Items"]
    )
    
    app.include_router(item_router)
    return TestClient(app)

Seeding Test Data

FastCRUD tests interact with the database through the same SQLModel/SQLAlchemy models used by the application. Insert test records directly using the async session before executing HTTP requests.

async def seed_test_data(session: AsyncSession):
    """Helper to populate the database with sample records."""
    test_items = [
        Item(name="Laptop", price=999.99, is_deleted=False),
        Item(name="Mouse", price=25.50, is_deleted=False),
        Item(name="Keyboard", price=75.00, is_deleted=False)
    ]
    for item in test_items:
        session.add(item)
    await session.commit()

Testing Core CRUD Operations

Verify that the generated endpoints correctly handle the full lifecycle of a resource. The default paths follow the convention: /items/create, /items/get/{id}, /items/update/{id}, and /items/delete/{id}.

@pytest.mark.asyncio
async def test_create_read_update_delete_flow(client, async_session):
    """Test the complete CRUD cycle including soft delete."""
    
    # ---------- CREATE ----------

    create_payload = {"name": "Monitor", "price": 299.99}
    response = client.post("/items/create", json=create_payload)
    assert response.status_code == 201
    created_item = response.json()
    assert created_item["name"] == "Monitor"
    item_id = created_item["id"]
    
    # ---------- READ SINGLE ----------

    response = client.get(f"/items/get/{item_id}")
    assert response.status_code == 200
    fetched = response.json()
    assert fetched["price"] == 299.99
    
    # ---------- UPDATE ----------

    update_payload = {"price": 249.99}
    response = client.patch(f"/items/update/{item_id}", json=update_payload)
    assert response.status_code == 200
    updated = response.json()
    assert updated["price"] == 249.99
    
    # ---------- DELETE (Soft) ----------

    response = client.delete(f"/items/delete/{item_id}")
    assert response.status_code == 204
    
    # Verify soft delete (item should be excluded from GET)

    response = client.get(f"/items/get/{item_id}")
    assert response.status_code == 404

Testing Pagination, Filtering, and Sorting

FastCRUD endpoints accept query parameters defined in the internal PaginatedRequestQuery and FilterConfig mechanisms. Test these by asserting on the data array returned in the paginated response structure.

@pytest.mark.asyncio
async def test_get_multi_with_query_params(client, async_session):
    """Test pagination, filtering, and sorting via query strings."""
    await seed_test_data(async_session)
    
    # Test pagination (page=1, itemsPerPage=2)

    response = client.get("/items/get_multi?page=1&itemsPerPage=2")
    assert response.status_code == 200
    result = response.json()
    assert len(result["data"]) == 2
    assert result["total"] == 3  # Total count before pagination

    
    # Test filtering by exact match

    response = client.get("/items/get_multi?name=Mouse")
    assert response.status_code == 200
    data = response.json()["data"]
    assert len(data) == 1
    assert data[0]["name"] == "Mouse"
    
    # Test filtering with operators (e.g., price greater than)

    response = client.get("/items/get_multi?price__gt=50")
    assert response.status_code == 200
    items = response.json()["data"]
    assert all(item["price"] > 50 for item in items)
    
    # Test sorting descending by price

    response = client.get("/items/get_multi?sort=-price")
    assert response.status_code == 200
    items = response.json()["data"]
    prices = [item["price"] for item in items]
    assert prices == sorted(prices, reverse=True)

Testing Custom Dependencies

If your production router includes custom dependencies (such as authentication), override them in the test fixture using FastAPI's app.dependency_overrides dictionary.

def override_auth():
    return {"user_id": "test-user", "role": "admin"}

@pytest.fixture
def client_with_auth(async_session):
    app = FastAPI()
    
    async def get_session():
        yield async_session
    
    router = crud_router(
        session=get_session,
        model=Item,
        create_schema=CreateItemSchema,
        dependencies=[Depends(some_auth_dependency)]  # Production dependency

    )
    
    app.include_router(router)
    app.dependency_overrides[some_auth_dependency] = override_auth
    
    return TestClient(app)

Summary

  • FastCRUD generates endpoints dynamically through crud_router() in fastcrud/endpoint/crud_router.py, requiring you to instantiate the router inside test fixtures rather than importing static routes.
  • Use an async SQLite engine and TestClient as demonstrated in tests/sqlmodel/conftest.py to provide isolated database sessions for each test.
  • Test the full HTTP contract including status codes, JSON response shapes, and the data wrapper object used in paginated responses.
  • Validate advanced features by passing query parameters (page, itemsPerPage, sort, field__operator) to get_multi endpoints and asserting on filtered results.
  • Override dependencies in your test app to bypass authentication or other FastAPI dependencies while testing the underlying CRUD logic.

Frequently Asked Questions

How do I test endpoints that require authentication in FastCRUD?

Override the authentication dependency in your test fixture using app.dependency_overrides. Map your production dependency callable to a mock function that returns a test user object or valid token. This allows you to test the CRUD logic without configuring a live identity provider.

Can I use a synchronous database session to test FastCRUD endpoints?

FastCRUD is built on async SQLAlchemy patterns, and the crud_router function expects an async session dependency. While you could theoretically wrap synchronous calls, the repository's own test suite in tests/sqlmodel/ uses AsyncSession exclusively with create_async_engine and aiosqlite to match the library's internal async repository implementation.

How do I verify soft-delete behavior in my tests?

First, create a record via POST, then delete it via DELETE and assert a 204 status. Subsequently, attempt a GET request to /get/{id} and verify it returns 404. If you need to test recovery or audit trails, query the database directly through your test session to check that is_deleted is True or deleted_at is populated, depending on your model configuration.

What is the best way to test complex filter combinations?

Construct URLs with multiple query parameters and assert on the returned dataset size and content. FastCRUD supports operators like __gt, __lt, __startswith, and __contains. Test each operator individually, then combine them (e.g., ?price__gt=10&price__lt=100&name__startswith=Pro) to ensure the filtering logic in fastcrud/core/ correctly applies AND conditions between parameters.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →