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
app.dependency_overrides[get_db] = fake_db
A dictionary on the app, mapping the real callable to a replacement. When FastAPI resolves a dependency it checks that dictionary first, and uses the stand-in if one is registered.
That is the whole feature. It needs no test framework, no patching, and 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 practical.
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
Output
Overriding authentication
The common case: tests that exercise an endpoint's logic without constructing a real token.
example_02.pyFastAPI
Output
It reaches the whole tree
Overriding a sub-dependency changes every dependency built on it, without touching them.
example_03.pyFastAPI
Output
Overriding a dependency replaces it wherever it appears, including deep inside a tree.
So overriding token at the bottom changes current_user and admin_only above it, without either being mentioned. That is usually what you want: fake the one thing at the edge — the network call, the database, the clock — and let the real logic above it run unchanged.
It is also the reason to fake as low as possible. Overriding admin_only skips the permission logic entirely; overriding token lets that logic actually be tested.
Router-level dependencies too
An override replaces the function wherever it is declared, including on a router.
example_04.pyFastAPI
Output
Putting it back
Overrides live on the app, so a test that does not clear up leaks into every test after it.
example_05.pyFastAPI
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
Output
Why it matters
Everything in this tier pushes requirements into dependencies: the database 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, because every test would then need a real database and a real token.
Overrides are the release valve. The endpoint keeps declaring Depends(get_db); the test decides what get_db means.
Two consequences worth noticing. Tests get faster, because nothing real is constructed. And tests get *narrower* — a test for a handler's logic is not also a test of authentication, which means a broken token service does not fail two hundred unrelated tests.
The key is the function object
This is the one thing that goes wrong, and the symptom is confusing: the override appears to be ignored.
The dictionary is keyed by identity, so 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, and those resolve to two different module objects, you have two different functions and the override targets the wrong one.
The fix is to be consistent about import paths. When an override silently does nothing, compare the two objects before looking anywhere else.
Router and app level too
An override replaces the function wherever it is declared, which includes dependencies=[...] on a router or the app.
That is what makes a router-wide authentication requirement pleasant to test. Without it, every test of every route under /admin would need a valid key.
Clean up
Overrides live on the app object, which usually outlives a single test.
A test that sets one and does not remove it changes every test that runs afterwards, and the failure appears somewhere unrelated with no obvious cause. In pytest the standard shape is a fixture that sets the override, yields, and clears it — the same setup/teardown discipline as a yield dependency, applied to the test.
app.dependency_overrides.clear() removes everything; del app.dependency_overrides[fn] removes one.
Beyond tests
Occasionally useful outside testing.
A local development override 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 override registered in production code is a piece of behaviour that does not appear in any endpoint's signature. If it is not a test, make it loud — guarded by an explicit setting, logged at startup, and impossible to enable by accident.
What this tier gives you
Dependencies let an endpoint declare what it needs. Sub-dependencies let those requirements compose. yield gives them a lifetime. Router-level declarations apply them to a section. Overrides let all of it be replaced at the edges.
Together that is most of what separates a FastAPI application that stays testable from one that does not — and none of it requires anything beyond functions and a default argument.
Mistakes people make
Two import paths for one function. The dictionary is keyed by identity, so from app.db import get_db and from .db import get_db can be different objects. The override then targets a function nobody depends on, and does nothing, silently.
Not clearing between tests. Overrides live on the app. One test that forgets makes a later, unrelated test fail with no visible cause.
Faking too high in the tree. Overriding admin_only skips the permission logic you meant to test. Override the edge - the token, the database, the clock - and let the real logic run.
Using them in production without saying so. An override is behaviour that appears in no endpoint signature. If it is not a test, gate it behind an explicit setting and log it at startup.
Forgetting they work on router-level dependencies. They do, which is what makes a section-wide auth requirement testable.
Overriding instead of designing. If a test needs six overrides, the endpoint probably depends on six things it should not.
The shape in pytest
The standard arrangement is a fixture that sets, yields and clears - the same discipline as a yield dependency:
Every test using that fixture gets the fake, and no test can leak it into the next one. It is four lines, and it is the difference between a suite that is trustworthy and one that fails differently depending on ordering.
What good test structure looks like
Overrides 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 override and clearing it. Tests then compose the ones they need rather than sharing a single do-everything client.
Fake at the edges. Override the database, the clock, the HTTP client, the token decoder. Do not override the permission logic, the filters, or anything you are 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 overriding is to remove 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
Overrides replace a dependency, and that is all they do.
They cannot change what an endpoint declares, so an endpoint depending on something unnecessary still depends on it in tests. They do not apply to code called *inside* a handler - a handler that imports and calls get_session() directly is untouched by any override, which is the strongest practical argument for declaring dependencies rather than reaching for them.
And they are per app object. Tests that construct their own FastAPI() per module get isolation 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 application without credentials for anything external. It is genuinely useful and it is one setting away from being enabled somewhere it should not be.
Demonstrations. Replacing a live data source with fixtures gives a stable demo that does not depend on the state of a shared environment.
The rule for both: an override registered outside a test is behaviour that appears in no endpoint signature and no schema. Gate it behind an explicit setting, log it loudly at startup, and make the default off. A reader of the code should not have to know the overrides exist to understand what an endpoint does.
What it says about the design
A final observation. If a test needs six overrides to run one endpoint, the overrides are not the problem - the endpoint is depending on six things.
Overrides make dependencies replaceable; they do not make an over-connected endpoint simple. When the fixture list grows, the useful question is whether the handler is doing work that belongs in a service, or depending on things it does not actually need.
Used that way the feature is also a design signal: the number of things you have to fake to test an endpoint is a fair measure of how entangled it is.
Why this closes the tier
The five modules in this tier fit together, and overrides are what make the arrangement practical rather than merely elegant.
Dependencies let an endpoint declare what it needs. Sub-dependencies let those requirements build on each other. yield gives them a lifetime. Router-level declarations apply them to a whole section.
Every one of those pushes real things - a database, a token service, a clock - further from the handler and closer to the edge of the application. Without a way to replace them, that would make the endpoints harder to test rather than easier, and the whole approach would be a net loss.
Overrides invert it. Because the edges are declared rather than reached for, they can be swapped, and 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 requirements instead of acquiring them. Everything else follows from it.
Summary
app.dependency_overrides[real] = fake replaces a dependency everywhere it appears, including in a tree and on a router.
The key is the function object, so inconsistent import paths are the usual reason an override silently does nothing. Overrides live on the app, so clear them between tests or one quietly changes the next.
Fake at the edges - the database, the clock, the token - and let the real logic above run. Keep one test with nothing overridden, so the fakes cannot drift from what they stand in for without something failing.
Summary, in one line
app.dependency_overrides[real] = fake swaps a dependency everywhere it appears - keyed by the function object, cleared between tests, and 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 override silently does nothing.
Clear between tests. A leaked override 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 requirements into dependencies moves real things - databases, tokens, clocks - to the edge of the application. That would make testing harder if the edge could not be replaced. Overrides are what make it replaceable, and therefore what makes the whole approach pay.
Next
The runtime: what actually happens when a request arrives, why an async def endpoint that makes a blocking call stalls every other request in the process, and the parts of the framework that need a real event loop.
And the failure to watch for is silence: an override that targets a different object than the endpoints use does nothing at all, reports nothing, and 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
A dictionary on the app, mapping the real callable to a replacement. When FastAPI resolves a dependency it checks that dictionary first, and uses the stand-in if one is registered.
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.