Swapping a dependency for a test - the piece that makes the rest of this tier testable without a database or a network.
Overview
The mechanism
A dependency override replaces one dependency with another, for the whole app, while it is running. Its main use is testing. Your real database dependency connects to a real database. During a test, you want it to return a fake or an in-memory one. An override swaps it without touching your endpoint code.
This is one of the best reasons to use dependency injection at all. Because endpoints ask for what they need instead of creating it, you can change what they receive from outside. There is no patching of modules and no tricks with imports. You set a single entry in a dictionary.
This article starts with the mechanism itself: the dictionary on the app, and what you put into it. It then explains why it matters for the way you design and test code. You will learn that the key is the function object, not its name, so you must reference the original function exactly as the endpoint did.
Next comes reach. An override applies to the whole dependency tree, so a replaced function is swapped everywhere it appears, even in sub-dependencies. It also works at the router and app levels. Overrides stay in place until you clear them, so the article closes with how to reset them after each test.
With this technique, tests become fast, isolated, and honest about what they check.
You will see short examples that you can paste into your own test files. The aim is to leave you with a pattern that works the same way on a small project and on a large one. Many teams adopt this single technique and find that their test suites become both faster and easier to read, because each test states plainly what it replaces and why. It also removes the need for patching tools that rely on import paths. Those tools tend to break when you move code, whereas an override keeps working because it points at the function itself. Each section is short and ends with a habit you can apply straight away, so you finish with working knowledge of this technique.
app.dependency_overrides[get_db] = fake_db
A dict on the app, mapping the real callable to a swap. When FastAPI resolves a dependency it checks that dict first. Uses the stand-in if one is registered.
That is the whole feature. It needs no test framework, no patching. No change to the endpoints.
Worth knowing
app.dependency_overrides[real] = fake replaces a dependency everywhere it is used.
The key is the function object, so it must be the same one the endpoints depend on — importing it twice by different paths is the usual reason an override appears not to work.
Overriding a sub-dependency changes everything built on it, without naming the intermediate ones.
It applies to router- and app-level dependencies as well as parameters.
Overrides live on the app, so clear them between tests — app.dependency_overrides.clear() — or one test quietly changes the next.
A yield dependency can be replaced by another, so a fake session still gets setup and teardown.
Dependency Overrides
Swapping a dependency for a test, and why that makes the whole tier hands-on.
Replacing one for a test
app.dependency_overrides maps the real function to a stand-in. Every endpoint using it gets the stand-in instead.
example_01.pyFastAPI
from fastapi import Depends, FastAPI
app = FastAPI()
def get_db(): # the real one would open a connection
raise RuntimeError("no database here")
@app.get("/modules")
def list_modules(db=Depends(get_db)):
return {"rows": db["rows"]}
def fake_db():
return {"rows": ["Vectors", "Norms"]}
print("without an override:")
try:
TestClient(app).get("/modules")
except RuntimeError as e:
print(" ", e)
app.dependency_overrides[get_db] = fake_db
print("with one :", TestClient(app).get("/modules").json())
Output
Overriding authentication
The common case: tests that exercise an endpoint's logic without constructing a real token.
example_02.pyFastAPI
from fastapi import Depends, FastAPI, Header, HTTPException
app = FastAPI()
def current_user(x_token: str = Header(default="")):
if x_token != "a-real-token":
raise HTTPException(401, "Sign in")
return {"name": "ada", "role": "reader"}
@app.get("/me")
def me(user: dict = Depends(current_user)):
return user
c = TestClient(app)
print("no token :", c.get("/me").status_code)
app.dependency_overrides[current_user] = lambda: {"name": "test", "role": "admin"}
print("overridden :", TestClient(app).get("/me").json())
print()
print("The endpoint is unchanged. Only what it depends on moved.")
Output
It reaches the whole tree
Overriding a sub-dependency changes every dependency built on it, without touching them.
example_03.pyFastAPI
from fastapi import Depends, FastAPI, Header, HTTPException
app = FastAPI()
def token(x_token: str = Header(default="")):
if not x_token:
raise HTTPException(401, "No token")
return x_token
def current_user(t: str = Depends(token)):
return {"name": t.upper()}
def admin(user: dict = Depends(current_user)):
if user["name"] != "ADA":
raise HTTPException(403, "Admins only")
return user
@app.get("/admin")
def admin_page(u: dict = Depends(admin)):
return u
c = TestClient(app)
print("no token :", c.get("/admin").status_code)
app.dependency_overrides[token] = lambda: "ada" # bottom of the tree
print("token faked :", TestClient(app).get("/admin").json())
print()
print("current_user and admin were never mentioned, and both changed.")
Output
replacing a dependency replaces it wherever it appears, including deep inside a tree.
So replacing token at the bottom changes current_user and admin_only above it, without either being mentioned. That is often what you want. Fake the one thing at the edge. The network call, the DB, the clock. And let the real logic above it run unchanged.
It's also the reason to fake as low as possible. replacing admin_only skips the permission logic fully. Replacing token lets that logic really be tested.
Router-level dependencies too
An override replaces the function wherever it is declared, including on a router.
Overrides live on the app, so a test that does not clear up leaks into every test after it.
example_05.pyFastAPI
from fastapi import Depends, FastAPI
app = FastAPI()
def flag():
return {"mode": "real"}
@app.get("/x")
def x(f: dict = Depends(flag)):
return f
c = TestClient(app)
print("before :", c.get("/x").json())
app.dependency_overrides[flag] = lambda: {"mode": "fake"}
print("during :", TestClient(app).get("/x").json())
app.dependency_overrides.clear() # or del [flag]
print("after :", TestClient(app).get("/x").json())
print()
print("A test that forgets this makes the next one fail for no visible reason.")
Output
A yield dependency can be overridden too
Including with another yield dependency, so a fake session gets the same setup and teardown.
example_06.pyFastAPI
from fastapi import Depends, FastAPI
app = FastAPI()
log = []
def get_db():
raise RuntimeError("no real database in a test")
@app.get("/rows")
def rows(db: list = Depends(get_db)):
return {"rows": db}
def fake_db():
log.append("open")
try:
yield ["a", "b"]
finally:
log.append("close")
app.dependency_overrides[get_db] = fake_db
print(TestClient(app).get("/rows").json())
print("lifecycle:", log)
Output
Why it matters
all in this tier pushes needs into dependencies. The DB session, the current user, the permission check, the filters. That is good design and it would be a problem if those were hard to replace. Each test would then need a real DB and a real token.
replaces are the release valve. The endpoint keeps declaring Depends(get_db). The test decides what get_db means.
Two results worth noticing. Tests get faster. Nothing real is made. And tests get *narrower*. A test for a handler's logic isn't also a test of authentication. This Means a broken token service doesn't fail two hundred unrelated tests.
The key is the function object
This is the one thing that goes wrong. The symptom is confusing: the replace appears to be skipped.
The dict is keyed by identity. The object you use as the key must be the same object the endpoints depend on. If your test imports get_db from app.db and the router imported it from .db. Those resolve to two other module objects, you've two other functions and the replace targets the wrong one.
The fix is to be steady about import paths. When an replace silently does nothing, compare the two objects before looking anywhere else.
Router and app level too
An replace replaces the function wherever it's declared. This Includes dependencies=[...] on a router or the app.
That is what makes a router-wide authentication need pleasant to test. Without it, each test of each route under /admin would need a valid key.
Clean up
replaces live on the app object. This Often outlives a single test.
A test that sets one and doesn't drop it changes each test that runs later. The failure appears one spot unrelated with no obvious cause. In pytest the standard shape is a fixture that sets the replace, yields. Clears it. The same setup/teardown discipline as a yield dependency, applied to the test.
app.dependency_overrides.clear() drops all. del app.dependency_overrides[fn] drops one.
Beyond tests
at times handy outside testing.
A local development replace can swap a real payment provider for a recording stub, or a real mailer for one that writes to a file.
A demo build can replace a live data source with fixtures.
Both are legitimate, and both deserve care: an replace registered in production code is a piece of action that doesn't appear in any endpoint's signature. If it isn't a test, make it loud. Guarded by an clear setting, logged at startup, and hopeless to enable by accident.
What this tier gives you
Dependencies let an endpoint declare what it needs. Sub-dependencies let those needs compose. yield gives them a lifetime. Router-level declarations apply them to a section. replaces let all of it be replaced at the edges.
Together that is most of what splits a FastAPI use that stays testable from one that doesn't. And none of it needs anything beyond functions and a default arg.
Mistakes people make with Dependency Overrides
Two import paths for one function. The dict is keyed by identity. from app.db import get_db and from .db import get_db can be other objects. The replace then targets a function no one depends on, and does nothing, silently.
Not clearing between tests. replaces live on the app. One test that forgets makes a later, unrelated test fail with no visible cause.
Faking too high in the tree. replacing admin_only skips the permission logic you meant to test. replace the edge - the token, the DB, the clock - and let the real logic run.
Using them in production without saying so. An replace is action that appears in no endpoint signature. If it isn't a test, gate it behind an clear setting and log it at startup.
Losing they work on router-level dependencies. They do. This Is what makes a section-wide auth need testable.
replacing instead of designing. If a test needs six replaces, the endpoint probably depends on six things it shouldn't.
The shape in pytest
The standard setup is a fixture that sets, yields and clears - the same discipline as a yield dependency:
each test using that fixture gets the fake. No test can leak it into the next one. It's four lines. It's the gap between a suite that is trustworthy and one that fails differently based on sorting.
What good test structure looks like
replaces work best with a small amount of structure around them.
One fixture per fake. A fake_db fixture, a fake_user fixture, each setting one replace and clearing it. Tests then compose the ones they need rather than sharing a single do-all client.
Fake at the edges. replace the DB, the clock, the HTTP client, the token decoder. Don't replace the permission logic, the filters, or anything you're trying to test.
Prefer real objects to mocks. A fake_db returning a dict or an in-memory list exercises more of the real code path than a mock that asserts it was called. The point of replacing is to drop the network, not the logic.
Keep one test with nothing overridden. An integration test that exercises the real tree catches the case where the fakes have quietly diverged from what they stand in for - which is the failure mode of heavy faking.
The limits
replaces replace a dependency, and that is all they do.
They can't change what an endpoint declares. An endpoint based on a thing not needed still depends on it in tests. They don't apply to code called *inside* a handler - a handler that imports and calls get_session() right is untouched by any replace. This Is the strongest hands-on arg for declaring dependencies rather than reaching for them.
And they're per app object. Tests that make their own FastAPI() per module get alone for free. Tests that share one imported app need the discipline of clearing.
Beyond the test suite
Two uses outside testing are legitimate, and both deserve care.
Local development. Swapping a payment provider for a recording stub, or a mailer for one that writes to a file, lets a developer run the whole use without credentials for anything external. It's truly handy and it's one setting away from being enabled one spot it shouldn't be.
Demos. Replacing a live data source with fixtures gives a stable demo that doesn't depend on the state of a shared setting.
The rule for both: an replace registered outside a test is action that appears in no endpoint signature and no schema. Gate it behind an clear setting, log it loudly at startup. Make the default off. A reader of the code shouldn't have to know the replaces exist to understand what an endpoint does.
What it says about the design
A final finding. If a test needs six replaces to run one endpoint, the replaces aren't the problem - the endpoint is based on six things.
replaces make dependencies replaceable. They don't make an over-connected endpoint simple. When the fixture list grows, the handy question is whether the handler is doing work that belongs in a service, or based on things it doesn't really need.
Used that way the feature is also a design signal. The number of things you've to fake to test an endpoint is a fair measure of how entangled it's.
Why this closes the tier
The five modules in this tier fit together. Replaces are what make the setup hands-on rather than merely elegant.
Dependencies let an endpoint declare what it needs. Sub-dependencies let those needs build on each other. yield gives them a lifetime. Router-level declarations apply them to a whole section.
each one of those pushes real things - a DB, a token service, a clock - further from the handler and closer to the edge of the use. Without a way to replace them, that would make the endpoints harder to test rather than easier. The whole approach would be a net loss.
replaces invert it. Because the edges are declared rather than reached for, they can be swapped. A handler that depends on four external things can be tested with none of them present.
That is the trade this tier is really about: declaring needs instead of acquiring them. all else follows from it.
Dependency Overrides: summary
app.dependency_overrides[real] = fake replaces a dependency all over it appears, including in a tree and on a router.
The key is the function object. Inconsistent import paths are the usual reason an replace silently does nothing. replaces live on the app. So Clear them between tests or one quietly changes the next.
Fake at the edges - the DB, the clock, the token - and let the real logic above run. Keep one test with nothing overridden. The fakes can't drift from what they stand in for without a thing failing.
Summary, in one line
app.dependency_overrides[real] = fake swaps a dependency all over it appears - keyed by the function object, cleared between tests. Applied as low in the tree as possible so the logic above it still runs.
Two rules
Key on the same object the endpoints use. Inconsistent imports are the reason an replace silently does nothing.
Clear between tests. A leaked replace fails a later, unrelated test with no visible cause.
Where it sits in the tier
Last of the five, and the one that justifies the other four.
Pushing needs into dependencies moves real things - DBs, tokens, clocks - to the edge of the use. That would make testing harder if the edge couldn't be replaced. replaces are what make it replaceable. So what makes the whole approach pay.
Questions people ask about Dependency Overrides
How do I replace a dependency in a test? Set app.dependency_overrides[original_function] = fake_function. FastAPI then calls the fake.
What is the key of the overrides dict? The original function object itself, not its name. It must be the same object used in Depends().
Does it work for sub-dependencies? Yes. The override applies anywhere in the tree where that dependency is used.
How do I undo it? Call app.dependency_overrides.clear() or use a pytest fixture that yields then clears.
Does the fake need the same parameters? No. The override gets its own signature, and FastAPI resolves that signature instead.
Is it only for tests? It is mostly for tests, but it also helps with local development against a stub service.
Where to go after Dependency Overrides
The runtime: what really happens when a request arrives, why an async def endpoint that makes a blocking call stalls every other request in the process. The parts of the framework that need a real event loop.
And the failure to watch for is silence. An replace that targets a other object than the endpoints use does nothing at all, reports nothing. Leaves the test passing against the real dependency.
Check yourself
0 of 4
Answer without scrolling back up.
What is `app.dependency_overrides` keyed by?
It is keyed by identity, so importing the same function by two different paths gives two objects and the override silently targets the wrong one.
You override a dependency at the bottom of a tree. What happens above it?
That is why you should fake as low as possible - overriding the top skips the logic you wanted to test, while overriding the edge lets it run.
Why clear overrides between tests?
The failure appears in an unrelated test with no obvious cause. A fixture that sets, yields and clears is the standard shape.
Can a router-level dependency be overridden?
Which is what makes a router-wide auth requirement testable; otherwise every test of every route in that section would need a valid credential.
Cheat sheet
Dependency Overrides
Swap a real dependency for a test double with dependency_overrides, run the test, and see why the pattern makes FastAPI simple to test.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.