Python API Development with FastAPI

Testing

Setup Test Database With Fixtures

In this lesson, we will learn how to set up a test database using fixtures and configure one fixture to depend on another. This approach enables you to create a fixture that returns a database session (or database object) and then pass that session fixture to another fixture that provides a configured TestClient. Dependency chaining like this simplifies database manipulation and client usage in your tests.

Why Use Fixture Dependency?

Using fixture dependency helps separate concerns by allowing one fixture to manage the database session while another focuses on providing an HTTP client. This setup ensures that tests run against a freshly configured database, improving test reliability.


Overriding the Database Dependency with a Client Fixture

Below is an initial example where we override the dependency for the database connection using a fixture that returns a TestClient:

@app.dependency_overrides[get_db] = override_get_db

@pytest.fixture
def client():
    Base.metadata.drop_all(bind=engine)
    Base.metadata.create_all(bind=engine)
    yield TestClient(app)

def test_root(client):
    res = client.get("/")
    print(res.json().get("message"))
    assert res.json().get("message") == "Hello World"
    assert res.status_code == 200

def test_create_user(client):
    res = client.post("/users/", json={"email": "[email protected]", "password": "password123"})

In this example, the fixture returns our client. However, if you need direct access to the database object, you can separate the responsibilities by creating a dedicated fixture (named session) to set up the database and return a database session. Then, pass this session fixture to the TestClient fixture.


Creating a Session Fixture and Chaining Dependencies

Below is an updated version where we define the session fixture for managing database setup (dropping and creating tables) and then configure the TestClient fixture to use this session:

@pytest.fixture
def session():
    Base.metadata.drop_all(bind=engine)
    Base.metadata.create_all(bind=engine)
    db = TestingSessionLocal()
    try:
        yield db
    finally:
        db.close()

@pytest.fixture
def client():
    yield TestClient(app)

def test_test_root(client):
    # This test uses the client fixture which initializes the database session
    res = client.get("/")

At this point, the test framework ensures that whenever the client fixture is used, the session fixture runs first. This guarantees a fresh database setup and an available session before tests execute. You can also access the session directly in your tests if necessary.

Here’s an enhanced version that demonstrates passing the session fixture into the TestClient fixture and using it in a test:

@pytest.fixture
def session():
    Base.metadata.drop_all(bind=engine)
    Base.metadata.create_all(bind=engine)
    db = TestingSessionLocal()
    try:
        yield db
    finally:
        db.close()

@pytest.fixture
def client(session):
    yield TestClient(app)

def test_root(client):
    res = client.get("/")
    print(res.json().get("message"))
    assert res.json().get("message") == "Hello World"

Overriding the Database Dependency Properly

Next, modify the client fixture to override the database dependency correctly. Instead of yielding a hard-coded database object, define an inner function override_get_db that yields the session fixture. Apply this override before returning the TestClient:

@pytest.fixture
def client(session):
    def override_get_db():
        try:
            yield session
        finally:
            session.close()
    app.dependency_overrides[get_db] = override_get_db
    yield TestClient(app)

def test_root(client):
    res = client.get("/")
    print(res.json().get("message"))
    assert res.json().get("message") == "Hello World"
    assert res.status_code == 200

With this setup, every time a test uses the client fixture, the session fixture is invoked first. This guarantees that the TestClient has access to a fresh database session, allowing direct database queries such as session.query(models.Post) when required.

For example, to access the database session separately from the client, include the session fixture in your test parameters:

@pytest.fixture
def client(session):
    def override_get_db():
        try:
            yield session
        finally:
            session.close()
    app.dependency_overrides[get_db] = override_get_db
    yield TestClient(app)

def test_root(client, session):
    res = client.get("/")
    print(res.json().get("message"))
    assert res.json().get("message") == "Hello World"
    assert res.status_code == 200

The client fixture now ensures that the session fixture executes beforehand, and the database dependency override is applied accordingly.


Organizing Database Configuration into a Separate File

After verifying that your tests work as expected (for example, running pytest tests/test_calculations.py in the terminal), consider cleaning up your test files by moving database-specific code and fixtures into a separate file (such as database.py) within your test directory.

An example of what the database.py file might look like:

from fastapi.testclient import TestClient
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.main import app
from app import schemas
from app.config import settings
from app.database import get_db, Base
from alembic import command

# SQLALCHEMY_DATABASE_URL
SQLALCHEMY_DATABASE_URL = (
    f'postgresql://{settings.database_username}:{settings.database_password}'
    f'@{settings.database_hostname}:{settings.database_port}/{settings.database_name}_test'
)

engine = create_engine(SQLALCHEMY_DATABASE_URL)

TestingSessionLocal = sessionmaker(
    autocommit=False, autoflush=False, bind=engine
)

@pytest.fixture
def session():
    Base.metadata.drop_all(bind=engine)
    Base.metadata.create_all(bind=engine)
    db = TestingSessionLocal()
    try:
        yield db
    finally:
        db.close()

@pytest.fixture
def client(session):
    def override_get_db():
        try:
            yield session
        finally:
            session.close()
    app.dependency_overrides[get_db] = override_get_db
    yield TestClient(app)

After moving the database configuration and fixtures into database.py, your test file (e.g., test_users.py) becomes more organized. It now only needs to import the necessary fixtures and define the test cases. For instance:

from app import schemas
from database import client, session

def test_root(client):
    res = client.get("/")
    print(res.json().get("message"))
    assert res.json().get("message") == "Hello World"
    assert res.status_code == 200

def test_create_user(client):
    res = client.post(
        "/users/", json={"email": "[email protected]", "password": "password123"}
    )
    new_user = schemas.UserOut(**res.json())
    assert new_user.email == "[email protected]"
    assert res.status_code == 201

After saving your changes, running your tests should confirm that both tests pass successfully. This organized setup not only cleans up your test files but also ensures that the database is properly configured and available during testing.

Deprecation Warning

If you encounter warnings such as "is deprecated since Python 3.8, use 'async def' instead," review any legacy non-fixture code. Since table creation and session management are now fully handled within the fixtures, these warnings should no longer apply.


This concludes our lesson on setting up a test database with fixtures. By leveraging fixture dependency, we efficiently manage both the TestClient and the database session, simplifying testing and ensuring a reliable, isolated test environment.

For additional information and advanced testing strategies, please refer to the FastAPI Testing Documentation.

Watch Video

Watch video content

Previous
Create Destroy Database After Each Test