FastAPI / Intermediate: application integration and security / Lesson 8
Testing and dependency isolation
Verify public behavior with isolated apps, controlled dependencies, and the appropriate synchronous or asynchronous test client.
On this page
Lesson goals
- Explain the assumptions and boundaries of testing and dependency isolation.
- Interpret both complete examples and justify their observed outputs.
- Diagnose the listed failures and apply the worked solutions while keeping identity, state, and lifecycle ownership explicit.
Test behavior through a real application boundary
A useful API test states a requirement, sends a representative request, and checks the resulting status, representation, and relevant effects. Testing only that a helper was called can miss broken routing or serialization. Use small pure unit tests for business rules and in-process integration tests for framework wiring. The first example overrides a synthetic provider, verifies the public response, clears the override, and checks the original behavior. The client exercises FastAPI without a socket server. It does not prove proxy, TLS, multiple-worker, database, or external-service behavior, so label that scope honestly.
from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
def source(): return "original-lab-value"
app = FastAPI()
@app.get("/value")
def value(text: Annotated[str, Depends(source)]): return {"value": text}
with TestClient(app) as client:
app.dependency_overrides[source] = lambda: "fixed-lab-value"
try:
response = client.get("/value")
assert response.status_code == 200
print(response.json())
finally:
app.dependency_overrides.clear()
print(client.get("/value").json())
{'value': 'fixed-lab-value'}
{'value': 'original-lab-value'}
The override changes only the selected provider and is removed in finally. The second request proves the original provider was restored.
Give fixtures explicit ownership
Create an isolated app and synthetic state for each relevant test scope. Cleanup belongs in finally, a context manager, or the test framework’s fixture teardown, including when an assertion fails. Dependency overrides are keyed by the original callable, so changing a similar-looking function does not necessarily replace the intended dependency. Avoid configured project databases, real credentials, and shared resource names. A test that passes alone but fails with neighbors often has state, order, environment, or lifecycle leakage. Fix that ownership before adding blind retries, which can hide the exact evidence needed to understand the defect.
Match client style to the test
TestClient is convenient for synchronous tests and uses an in-process transport. When a test itself needs await, HTTPX AsyncClient with ASGITransport can exercise the application inside an async function. The second example uses asyncio.run only at the script entry point. ASGITransport does not automatically run application lifespan, so a test with startup resources needs explicit lifespan management or a different appropriate fixture. This sample has no such resource and states that scope. Do not mix event loops or reuse an async client across incompatible lifetimes simply because the object is available globally.
import asyncio
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
app = FastAPI()
@app.get("/health")
async def health(): return {"ok": True}
async def main():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://lab.invalid") as client:
response = await client.get("/health")
assert response.status_code == 200
print(response.json())
asyncio.run(main())
{'ok': True}
The reserved hostname is only a URL context; ASGITransport sends the request directly to the local app. No DNS or network connection occurs, and this app has no lifespan resource.
Assert failure and security boundaries
Include invalid input, missing resources, unauthorized and forbidden requests, response filtering, and cleanup. TestClient’s default server-exception behavior helps expose programming failures; use a deliberate option when testing the public error response instead. Assert stable error codes and locations rather than an entire traceback. A dependency fake should make the scenario controllable without deleting the actual policy or transport boundary being tested. For example, inject a trusted synthetic principal and still execute the real ownership check. Otherwise the test can prove a fictional authorization path that production never uses.
Keep proof repeatable and limited
Record the pinned dependency baseline and avoid tests whose correctness depends on arbitrary sleeps or real network timing. Use returned promises, explicit state, or controlled async scheduling to establish completion. A high coverage number does not prove meaningful assertions, so ask whether a plausible regression would fail each important test. These scripts are complete and use only synthetic providers and internal transports. They start no server and touch no configured storage. Real database and deployment guarantees require separately authorized tests; local client success must not be reported as production verification.
Common errors and fixes
A test uses the real project dependency
- Why it happens
- The wrong callable or no override was selected.
- How to fix it
- Inject a safe provider and verify the actual dependency key.
- Prevent it next time
- Use isolated app fixtures and explicit no-external-service policy.
Async tests miss startup state
- Why it happens
- ASGI transport was assumed to manage lifespan.
- How to fix it
- Manage lifespan explicitly for resource-owning apps.
- Prevent it next time
- Test acquisition and cleanup around the client lifetime.
Tests pass after disabling the real authorization check
- Why it happens
- The fake replaced the policy under test.
- How to fix it
- Override identity or storage inputs while retaining the real policy.
- Prevent it next time
- Include unrelated-user and denied-operation cases.
Check your understanding
1. Which dependency should be overridden when testing a resource ownership rule?
Show worked solution
Override the identity verifier or repository with controlled synthetic inputs while keeping the ownership policy active. Assert the owner succeeds and another user is denied. Replacing the policy with always true would test the fake rather than protect the security contract.
2. Why is an arbitrary sleep a weak way to wait for background completion in a test?
Show worked solution
It adds delay without proving the intended operation completed and can fail under different scheduling. Use a controlled completion signal or inspect an explicitly owned result at the supported boundary. Keep real distributed background guarantees outside an in-memory test claim.
Key takeaways
- A useful API test states a requirement, sends a representative request, and checks the resulting status, representation, and relevant effects.
- TestClient is convenient for synchronous tests and uses an in-process transport.
- Record the pinned dependency baseline and avoid tests whose correctness depends on arbitrary sleeps or real network timing.