Mocking and Patching
Scenario
Your CI pipeline is failing randomly because an external payment API (Stripe) is rate-limiting your test runner. Tests shouldn't hit real external APIs.
Mental model
Mocking is like a movie set. Instead of using a real bank vault (an external API), you build a wooden prop that looks like a vault. The actors (your code) interact with the prop, and you control exactly what the prop does.
Deep dive
In Python, `unittest.mock.patch` temporarily replaces a real object (like a function or a class) with a `MagicMock` during a test. A mock records how it was called and can be configured to return specific values.
FastAPI provides a superior alternative to `patch` for APIs: `app.dependency_overrides`. Instead of monkey-patching Python's internals, you tell the framework to inject a fake dependency when running tests.
Code examples
Bad: Patching internals
from unittest.mock import patch
@patch('app.services.payment.charge_stripe')
def test_payment(mock_charge):
mock_charge.return_value = True
# Brittle: if the import path changes, the mock breaks
res = process_order()
mock_charge.assert_called_once()
Patching requires knowing exactly where the module is imported. It is highly brittle and can cause weird side effects if not cleaned up.
Good: FastAPI Dependency Overrides
from fastapi.testclient import TestClient
from app.main import app
from app.dependencies import get_payment_gateway
class FakeGateway:
def charge(self, amount):
return True
# Override the dependency
app.dependency_overrides[get_payment_gateway] = FakeGateway
client = TestClient(app)
def test_payment_endpoint():
res = client.post('/checkout')
assert res.status_code == 200
# Clean up override after test
app.dependency_overrides = {}
Instead of `patch`, we override the FastAPI dependency. This is typesafe, refactor-friendly, and respects the framework's lifecycle.
Common mistakes
- Patching the wrong path: You must patch the object where it is *used* (imported into), not where it is *defined*. If module `A` imports `foo` from `B`, you patch `A.foo`, not `B.foo`.
- Using patch instead of dependency_overrides: In FastAPI, patching internal functions that are meant to be injected is an anti-pattern. Always use `app.dependency_overrides` instead.
Recall questions
- What is the primary rule for the string path passed to `@patch()`?
- How do you check if a mocked function was executed exactly once?
- In FastAPI, what is the preferred way to mock a database session or external service?
Questions & answers
How do you test an endpoint that sends an email without actually sending an email?
If the email sender is injected via FastAPI `Depends`, I would use `app.dependency_overrides` to inject a `FakeEmailSender` that just appends the email to an in-memory list. Then I can assert the list length.
Why is FastAPI's dependency overriding superior to `unittest.mock.patch`?
`patch` relies on string paths which break during refactoring and can cause state leakage if not torn down. Dependency overrides are strongly typed, robust against refactoring, and explicitly supported by the framework.
Continue learning
Previous: Pytest Basics and Fixtures