Hello! Welcome back to our module on "Testing and Production Deployment."
In the last lesson, we laid a critical foundation for testing your FastAPI applications. We saw how to use TestClient and httpx.AsyncClient to make requests and, most importantly, how to use app.dependency_overrides to replace your production database dependency with a connection to an isolated, in-memory test database. This allows for safe and repeatable integration tests.
Today, we'll build directly on that knowledge to tackle a different but related testing strategy. Instead of swapping a dependency with another real (but temporary) implementation, what if we could replace it with a complete "fake" or "mock" object? This is essential when dealing with dependencies that are slow, costly, or have side effects, like sending emails or processing payments.
This lesson addresses the learning outcome: Use dependency overrides in tests to inject mocks for external services or database sessions.
This concept is analogous to using Python's unittest.mock.patch in Django to intercept and fake function calls. However, as you'll see, FastAPI's dependency injection system provides a more explicit and structured way to achieve this, making your tests clearer and more robust.
Unit Tests vs. Integration Tests: Why Mock?
In our previous lesson, by connecting to an in-memory SQLite database, we were performing what is often called a component or integration test. We were testing that our API endpoint worked correctly together with a real database. This is valuable because it verifies the entire interaction.
However, there's another type of test: a unit test. The goal of a unit test is to test the smallest possible "unit" of code in complete isolation. For an API endpoint, this means testing the logic within the endpoint itself, without relying on any external systems like a database.
Why would we want to do this?
- Speed: Unit tests are lightning-fast because they don't perform any I/O (like database queries or network requests).
- Focus: If a unit test fails, you know the problem is in the code unit itself, not in the database or another service it connects to.
- Control: You can easily simulate edge cases (like a database error or an external API being down) that are hard to trigger with real systems.
The video below discusses this exact distinction, arguing for the value of unit tests that mock the database instead of using a test instance.
FastAPI - Unit Testing with Database - Whats the correct approach? Mock vs. Local Database
This video from Coding Crash Courses makes a compelling case for using mocks to achieve true unit testing for API endpoints, contrasting it with the integration test approach.
Watch the intro (00:00 - 01:12) and the conclusion (04:29 - 05:07). The key idea is that for a unit test, you want to test your own code's logic, not the functionality of a library like SQLAlchemy, which is already tested by its developers.
The core idea is that for a unit test, we trust that db.add(item) works. What we want to verify is that our endpoint logic correctly calls db.add(item) under the right conditions. This is where mocks come in.
How to Mock a Dependency
The process of mocking a dependency is almost identical to what we did in the last lesson. The only difference is what we're swapping in.
- Create a mock object: We can use
MagicMockfrom Python's standardunittest.mocklibrary. AMagicMockobject will record any calls made to it and let you make assertions about those calls. - Create an override dependency: This function will
yieldour mock object instead of a real database session. - Apply the override: Use
app.dependency_overridesto tell FastAPI to use our mock dependency.
Let's see this in practice.
FastAPI - Unit Testing with Database - Whats the correct approach? Mock vs. Local Database
The same video provides a concise demonstration of how to implement this mocking strategy using MagicMock and app.dependency_overrides.
Watch from 01:57 to 04:29. Focus on these three parts: How MagicMock is used to create a mock_session. The override_get_db function is defined to yield this mock object. Most importantly, notice the assertions in the test. Instead of checking the database state, the test asserts that methods on the mock object were called: mock_db.add.assert_called() and mock_db.commit.assert_called().
This is the fundamental shift in mindset when testing with mocks: you test for interactions, not for state. You are verifying that your code attempted to perform the correct action on its dependency.
Here is the pattern in code:
from unittest.mock import MagicMock
from fastapi.testclient import TestClient
from .main import app, get_db
# 1. Create a mock object
mock_db_session = MagicMock()
# 2. Create an override dependency function
def override_get_db():
try:
yield mock_db_session
finally:
# Optional: reset the mock for the next test
mock_db_session.reset_mock()
# 3. Apply the override
app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)
def test_create_item():
# Act: Make the API call
response = client.post(
"/items/",
json={"name": "Test Item", "description": "A test description"}
)
# Assert: Check the API response
assert response.status_code == 200
assert response.json()["name"] == "Test Item"
# Assert: Check the interaction with the mock dependency
mock_db_session.add.assert_called_once()
mock_db_session.commit.assert_called_once()
Mocking at a Higher Level: Repositories and Services
Mocking a raw database session is effective, but it couples your tests to the low-level details of SQLAlchemy (.add(), .commit(), etc.). This aligns with your goal of learning modern software architecture. A more robust and professional pattern is to abstract your data access logic into a repository layer.

When you have a repository, your endpoint depends on get_repository instead of get_db. Your tests can then mock the entire repository, which is often cleaner. Instead of mocking .add() and .commit(), you can mock a single method like .create_item() and have it return a predictable result.
This approach makes your tests easier to read and more resilient to changes in the underlying database logic.
Combining FastAPI Dependency Injection with Service and Repository Layers
This article from dotcs.me perfectly demonstrates how a layered architecture (using repositories) makes testing with mocks incredibly clean and powerful.
Read the section titled 'Testing', focusing on the second example under 'test_controller.py'. Observe how they: Define a MockRepository class that mimics the interface of the real repository. Create a mock_get_repository dependency override function. Apply the override: app.dependency_overrides[get_repository] = mock_get_repository. The test can now assert against the predictable data returned by the MockRepository, completely isolating the controller's logic.
This is a powerful demonstration of how good architecture and FastAPI's dependency injection system work together to create highly testable code. You can test your controller logic by mocking the repository, and separately, you can write integration tests for your repository to ensure it interacts with the database correctly.
Bringing It All Together in Pytest
In a real project, you'll manage these overrides using pytest fixtures for a clean setup. We saw this pattern in the previous lesson, and it applies just as well to mocks.
The article "FastAPI, Furious Tests" shows how to create an app fixture that applies the dependency override. While it uses a test database, the principle is the same for mocks.
# From tests/conftest.py (conceptual example)
import pytest
from unittest.mock import MagicMock
# ... import your app and real dependency
from app.main import create_app
from app.services import get_email_service
@pytest.fixture
def mock_email_service():
return MagicMock()
@pytest.fixture
def app(mock_email_service):
"""Create a FastAPI app with the email service mocked."""
app = create_app()
# Override the dependency
app.dependency_overrides[get_email_service] = lambda: mock_email_service
return app
# Your test file can then use the 'app' and 'mock_email_service' fixtures
def test_signup_sends_email(client, mock_email_service):
client.post("/signup", json={"email": "test@example.com"})
mock_email_service.send_welcome_email.assert_called_once_with("test@example.com")
This shows how you'd mock an external service, like one that sends emails. Your tests run quickly without sending any actual emails, and you can precisely verify that your endpoint called the service correctly.
Conclusion
You have now mastered dependency_overrides, one of the most powerful features in FastAPI's testing toolkit. You understand not just how to use it, but also why you might choose different strategies for different testing scenarios.
Key Takeaways:
- Mocks for Unit Testing: Mocks allow you to test a unit of code (like an API endpoint) in complete isolation from its dependencies, leading to faster and more focused tests.
- Test Interactions, Not State: When using mocks, you typically assert that specific methods on the mock object were called with the correct arguments (e.g.,
mock.method.assert_called_with(...)). app.dependency_overridesis the Mechanism: This dictionary is your single point of entry for replacing any dependency in your application during tests.- Mock at the Right Abstraction: While you can mock low-level dependencies like database sessions, it's often cleaner to mock higher-level abstractions like repositories or services. This aligns well with modern architectural patterns you're aiming to learn.
- Fixtures for Cleanliness: Use
pytestfixtures to encapsulate your app creation and dependency overriding logic, keeping your test functions clean and focused on the test itself.
Preview of the Next Lesson:
So far, all our tests have been for public, unauthenticated endpoints. What happens when an endpoint is protected and requires a valid user token? How do we test that?
In the next lesson, we will learn how to write tests for authenticated endpoints by managing test tokens and authorization headers. You'll see how dependency injection once again proves invaluable, allowing us to easily inject a "test user" into our protected endpoints.