Hello! In our last lesson, we mastered the fundamentals of testing FastAPI endpoints by asserting status codes and response content. This is a vital skill, but our tests had a hidden flaw: they would interact with our main application database.
This lesson addresses that problem directly. We will learn how to override dependencies during testing to mock external services like databases. This allows us to run our tests against a separate, temporary database, making them faster, more reliable, and completely isolated from our production data. This is a standard practice in professional software development and is essential for building robust, testable applications.
By the end of this lesson, you will be able to configure a separate test database and use FastAPI's dependency override system to ensure your tests are completely self-contained.
1. The Case for a Separate Test Database
In the previous lesson, our tests for the create_message endpoint were writing to the same in-memory dictionary db that a live version of the application would use. While this worked for a simple example, imagine if db were a real production database. Running tests against it would be:
- Slow: Network latency to a real database server adds significant time to every test.
- Risky: A bug in a test could accidentally delete or corrupt real user data.
- Unreliable: Tests can fail if the database state is changed by other processes (or other tests).
- Complicated: Tests can't run in parallel if they all modify the same data, and you'd need complex cleanup scripts.
The solution is to mock the database dependency. During testing, we will swap out the "real" database connection with a connection to a temporary, in-memory database that exists only for the duration of the test run.
2. Preparing for Overrides: Dependency Injection
To swap out the database, our application code can't be hard-coded to create its own database session inside each endpoint. Instead, the session must be provided as a dependency. You've already used this pattern when building the database-backed parts of your application.
Let's review the pattern. Instead of this:
# Un-testable code
@app.post("/items/")
async def create_item(item: ItemCreate):
# Session is created directly inside the endpoint
async with AsyncSessionLocal() as session:
# ... database logic ...
return db_item
We use a dependency function to provide the session:
# database.py
# ... (engine and AsyncSessionLocal setup) ...
async def get_db():
async with AsyncSessionLocal() as session:
yield session
# main.py
from .database import get_db
@app.post("/items/")
async def create_item(item: ItemCreate, db: AsyncSession = Depends(get_db)):
# The session 'db' is now injected by FastAPI
# ... database logic ...
return db_item
This change is the key that unlocks testability. Because FastAPI is now in charge of calling get_db to provide the session, we can tell it to call a different function during tests.
3. Configuring a Test Database
Our first step is to define the configuration for our test database. We'll use SQLite's in-memory capabilities, which is perfect for fast, isolated testing.
In your test setup (typically in a tests/conftest.py file, as we'll see later), you will define a new SQLAlchemy engine and session maker specifically for testing.
The following video explains how to set up a test database environment for an async FastAPI application. It covers creating a test database URL, a test engine, and importantly, how to prepare for overriding the session dependency.
Testing A FastAPI App With Pytest
Watch this clip from 'Testing A FastAPI App With Pytest' by Ssali Jonathan. It walks through the complete process of setting up a test database and preparing the dependency override.
Watch from 0:54:14 to 1:05:46. Pay close attention to these key steps: Creating a separate test database URL. Creating a new async engine (test_engine) for the test database. Creating an override function (test_get_session) that yields a session from the test database. Using app.dependency_overrides to swap the real dependency with the test one.
As the video demonstrates, the core setup for a test database looks like this:
# In your test configuration file (e.g., tests/conftest.py)
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
# ... other imports
# Use an in-memory SQLite database for testing
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
test_engine = create_async_engine(
TEST_DATABASE_URL,
connect_args={"check_same_thread": False}, # Needed for SQLite
)
TestingSessionLocal = async_sessionmaker(
autocommit=False, autoflush=False, bind=test_engine
)
Now we have a test_engine and TestingSessionLocal that are completely separate from our main application's database configuration.
4. Overriding the Dependency with app.dependency_overrides
This is where the magic happens. FastAPI provides a simple but powerful mechanism for swapping dependencies: the app.dependency_overrides attribute. It's a dictionary where you map the original dependency function to the function you want to use instead.
The official FastAPI documentation provides the clearest explanation of this feature.
Testing Dependencies with Overrides - FastAPI
The official documentation, 'Testing Dependencies with Overrides', is the best source for understanding the core concept. It explains the 'why' and shows the exact syntax.
Read the sections 'Overriding dependencies during testing' and 'Use the app.dependency_overrides attribute'. The code example, while not about databases, perfectly illustrates the simple dict-like assignment that powers this feature.
To apply this to our database, we first create an "override" dependency function that uses our TestingSessionLocal.
# In your test configuration file
from app.database import get_db # The original dependency
# Override dependency that provides a test database session
async def override_get_db():
async with TestingSessionLocal() as session:
yield session
Then, before running our tests, we populate the dependency_overrides dictionary on our FastAPI app instance.
from app.main import app # Your FastAPI app instance
# The key is the original dependency, the value is our override
app.dependency_overrides[get_db] = override_get_db

Now, whenever an endpoint in your application asks for Depends(get_db), FastAPI will call override_get_db instead, and your endpoint will receive a session connected to the in-memory test database.
5. Managing Test Database State with Fixtures
Our test database is empty. To make it usable, we need to:
- Create the necessary tables before our tests run.
- Drop all tables after our tests are finished to ensure a clean slate for the next run.
pytest fixtures are the perfect tool for managing this setup and teardown process. We can create a fixture that handles table creation, applies the dependency override, yields a TestClient, and then cleans everything up.
This is a common and robust pattern. Let's combine everything into a single, powerful fixture in tests/conftest.py.
# tests/conftest.py
import pytest
from typing import AsyncGenerator
from fastapi.testclient import TestClient
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from app.main import app
from app.database import get_db, Base # Import your ORM Base
# --- Test Database Setup ---
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
test_engine = create_async_engine(
TEST_DATABASE_URL,
connect_args={"check_same_thread": False},
)
TestingSessionLocal = async_sessionmaker(
autocommit=False, autoflush=False, bind=test_engine
)
# --- Dependency Override ---
async def override_get_db() -> AsyncGenerator:
async with TestingSessionLocal() as session:
yield session
app.dependency_overrides[get_db] = override_get_db
# --- Test Client Fixture ---
@pytest.fixture(scope="function")
async def client() -> AsyncGenerator[TestClient, None]:
async with test_engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
# Use 'with' statement for TestClient to handle startup/shutdown events
with TestClient(app) as c:
yield c
async with test_engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
# Clean up the override after tests are done
app.dependency_overrides.clear()
With this fixture, any test function that accepts client as an argument will automatically get a TestClient configured to use a fresh, in-memory database.
Your tests now look the same as before, but they are secretly much more powerful:
# tests/test_items.py
# The 'client' fixture handles all the database setup and override!
async def test_create_and_read_item(client):
# Act 1: Create an item
response_create = client.post(
"/items/", json={"name": "Test Item", "description": "A test description"}
)
# Assert 1
assert response_create.status_code == 200
data = response_create.json()
assert data["name"] == "Test Item"
item_id = data["id"]
# Act 2: Read the item back
response_read = client.get(f"/items/{item_id}")
# Assert 2
assert response_read.status_code == 200
assert response_read.json() == data
This test proves the override is working. The item created via the POST request is successfully retrieved by the GET request because both operations are happening on the same temporary, in-memory database.
6. Practical Exercise
Time to apply these concepts. Assume you have an application for managing tasks.
app/models.py:
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import declarative_base
Base = declarative_base()
class Task(Base):
__tablename__ = "tasks"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True)
app/main.py (simplified):
# ... (app, schemas, and other imports)
from .database import get_db, Base, engine
from . import models
models.Base.metadata.create_all(bind=engine) # Creates tables on startup
@app.post("/tasks/", response_model=schemas.Task)
async def create_task(task: schemas.TaskCreate, db: AsyncSession = Depends(get_db)):
db_task = models.Task(title=task.title)
db.add(db_task)
await db.commit()
await db.refresh(db_task)
return db_task
@app.get("/tasks/{task_id}", response_model=schemas.Task)
async def read_task(task_id: int, db: AsyncSession = Depends(get_db)):
db_task = await db.get(models.Task, task_id)
if db_task is None:
raise HTTPException(status_code=404, detail="Task not found")
return db_task
Your task is to:
- Create a
tests/conftest.pyfile. - Implement the full
clientfixture as shown in section 5, ensuring it creates and drops theTasktable and overrides theget_dbdependency. - Create a
tests/test_tasks.pyfile. - Write a single async test function named
test_create_and_read_taskthat accepts theclientfixture. - Inside the test, first make a
POSTrequest to/tasks/to create a new task. - Assert that the status code is
200and the response contains the correct title. - Get the
idfrom the creation response, then make aGETrequest to/tasks/{id}. - Assert that this second request also returns a
200status code and that the task data matches what you created.
Click to see the solution
tests/conftest.py:
import pytest
from typing import AsyncGenerator
from fastapi.testclient import TestClient
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from app.main import app
from app.database import get_db
from app.models import Base # Make sure to import your Base from models
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
test_engine = create_async_engine(
TEST_DATABASE_URL,
connect_args={"check_same_thread": False},
)
TestingSessionLocal = async_sessionmaker(autocommit=False, autoflush=False, bind=test_engine)
async def override_get_db() -> AsyncGenerator:
async with TestingSessionLocal() as session:
yield session
app.dependency_overrides[get_db] = override_get_db
@pytest.fixture(scope="function")
async def client() -> AsyncGenerator[TestClient, None]:
async with test_engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
with TestClient(app) as c:
yield c
async with test_engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
app.dependency_overrides.clear()
tests/test_tasks.py:
from fastapi.testclient import TestClient
async def test_create_and_read_task(client: TestClient):
# Create a task
response_create = client.post("/tasks/", json={"title": "Test the database"})
assert response_create.status_code == 200
created_data = response_create.json()
assert created_data["title"] == "Test the database"
assert "id" in created_data
task_id = created_data["id"]
# Read the task back
response_read = client.get(f"/tasks/{task_id}")
assert response_read.status_code == 200
read_data = response_read.json()
assert read_data["id"] == task_id
assert read_data["title"] == "Test the database"
Conclusion
You have now learned one of the most important techniques for professional API testing. By isolating your application from external services like databases, you make your tests faster, more reliable, and completely independent.
Key Takeaways:
- Never run tests against a live production database.
- Use dependency injection for services like database sessions to make your code testable.
- Use FastAPI's
app.dependency_overridesdictionary to swap a real dependency with a test-specific version. - An in-memory SQLite database is an excellent choice for fast, isolated database testing.
pytestfixtures are the ideal tool for managing the setup (creating tables, applying overrides) and teardown (dropping tables, clearing overrides) of your test environment.
In our next lesson, we will apply this same concept to another common scenario: testing protected endpoints. We will learn how to override authentication dependencies to provide mock user credentials, allowing us to test secure endpoints without needing a real user login flow.