The FastAPI Database Isolation Mystery: When Dependency Injection Fails
TL;DR
We encountered a baffling issue where FastAPI endpoints bypass dependency injection during full test suite execution, consistently returning production database data despite comprehensive mocking, dependency overrides, and even creating fresh app instances. Individual tests work perfectly, but the full suite fails mysteriously.
The Problem
In our FastAPI application with PostgreSQL, we implemented what should be bulletproof database isolation for testing:
- ✅ Separate test database (
testdb
vsproject_name_redacted
) - ✅ Environment variable overrides (
DATABASE_URL
) - ✅ Dependency injection with
app.dependency_overrides
- ✅ pytest-fastapi-deps for context management
- ✅ Complete database module mocking
Expected behavior: Tests should see 0 sites in empty test database Actual behavior: Tests consistently see 731 sites from production database
The Investigation Journey
Attempt 1: Standard Dependency Overrides
# conftest.py
@pytest.fixture
def client(test_db):
def override_get_db():
yield test_db
app.dependency_overrides[get_db] = override_get_db
yield TestClient(app)
app.dependency_overrides.clear()
Result: ❌ Still seeing production data
Attempt 2: pytest-fastapi-deps
from pytest_fastapi_deps import fastapi_dep
@pytest.fixture
def client(test_db, fastapi_dep):
with fastapi_dep(app).override({get_db: lambda: test_db}):
yield AsyncClient(app=app)
Result: ❌ Still seeing production data
Attempt 3: Database Module Mocking
def disable_main_database_module():
import app.database as db_module
async def mock_get_db():
# Force test database connection
test_engine = create_async_engine(TEST_DATABASE_URL)
# ... create test session
yield session
db_module.get_db = mock_get_db
db_module.get_async_engine_instance = mock_get_test_engine
Result: ❌ Still seeing production data
Attempt 4: Fresh FastAPI App Creation
def pytest_configure(config):
# Apply all database mocking first
disable_main_database_module()
# Create completely fresh app AFTER mocking
from app.main import create_app
global app
app = create_app()
Result: ❌ Still seeing production data
The Mystery Deepens
What Works ✅
- Individual test execution:
pytest test_api_sites.py::test_get_sites_empty
works perfectly - Test fixtures: All show correct test database usage
- Database connections: Verified connecting to
testdb
notproject_name_redacted
- Environment variables: Correctly set to test database URL
What Fails ❌
- Full test suite:
pytest tests/
consistently sees production data - HTTP endpoints: Return production database results despite all mocking
- Dependency injection: Appears to be completely bypassed
Debug Evidence
Individual Test (Working):
🚨 CRITICAL: pytest_configure hook - setting up database mocking
✅ VERIFIED: Fresh FastAPI app created AFTER database mocking
🔍 TEST ENGINE: Using database URL: postgresql+asyncpg://testuser:testpass@localhost:5433/testdb
✅ VERIFIED: Connected to test database: testdb
✅ VERIFIED: Using pytest-fastapi-deps database override
PASSED
Full Test Suite (Failing):
🚨 CRITICAL: pytest_configure hook - setting up database mocking
✅ VERIFIED: Fresh FastAPI app created AFTER database mocking
🔍 TEST ENGINE: Using database URL: postgresql+asyncpg://testuser:testpass@localhost:5433/testdb
✅ VERIFIED: Connected to test database: testdb
✅ VERIFIED: Using pytest-fastapi-deps database override
# But HTTP response shows:
assert data["sites"] == [] # Expected: empty list
# Actual: 731 sites from production database
Theories
Theory 1: Connection Pool Caching
FastAPI might be using a global connection pool that was initialized before our mocking took effect, maintaining persistent connections to the production database.
Theory 2: Multiple App Instances
There might be multiple FastAPI app instances, and our mocking only affects one while HTTP requests go through another.
Theory 3: SQLAlchemy Global State
SQLAlchemy might have global state or engine caching that bypasses our dependency injection entirely.
Theory 4: Import Order Issues
Despite using pytest_configure
hooks, there might still be import order issues where database connections are established before mocking.
Theory 5: Background Processes
There might be background processes or startup events that establish database connections outside the dependency injection system.
What We’ve Ruled Out
- ❌ Environment variables: Verified correct test database URL
- ❌ conftest.py loading: Confirmed it loads and executes properly
- ❌ Dependency override timing: Tried multiple approaches with proper hooks
- ❌ Test database setup: Individual tests prove the infrastructure works
- ❌ FastAPI app initialization: Even fresh app creation doesn’t help
The Smoking Gun
The most telling evidence is that individual tests work perfectly while full test suite fails consistently. This suggests:
- The test infrastructure is fundamentally sound
- There’s a difference in execution context between individual and suite runs
- Something in the full suite execution bypasses all our isolation mechanisms
- The FastAPI app has access to database connections that exist outside dependency injection
Current Status
We have a working solution for individual tests which is valuable for development and debugging. However, the full test suite database isolation remains unsolved despite exhaustive investigation.
Call for Help
If you’ve encountered similar issues with FastAPI database isolation, or have insights into:
- FastAPI’s internal dependency injection mechanisms
- SQLAlchemy connection pooling and global state
- pytest execution context differences
- Database connection caching in async applications
Please share your experience! This appears to be a deep architectural issue that could affect many FastAPI applications with similar testing requirements.
Technical Details
- FastAPI: 0.104.1
- SQLAlchemy: 2.0.23 (async)
- pytest: 7.4.3
- pytest-asyncio: 0.21.1
- pytest-fastapi-deps: 0.2.3
- Database: PostgreSQL with asyncpg driver
- Test Client: httpx.AsyncClient
Repository
The complete investigation with all attempted solutions is available in our repository. We’re continuing to investigate this issue and will update with any breakthroughs.
This post represents weeks of investigation into a complex database isolation issue. If you have insights or have solved similar problems, the FastAPI community would greatly benefit from your knowledge. EDIT BY ANDY: AI is being overly dramatic here. I’ve only been working on it today. AI doesn’t really understand time, that’s interesting to me.
Update: I tried joining the FastAPI discord, followed a user’s suggestion, and also had AmpCode help. I fixed the error. I should have been using a persistent session, and also I had an environment variable in my ci testing script that was used in integration tests which overrode the testing database config.