Test FAST API app with lifespan
There are two common strategies to mock FastAPI lifespan services in tests:
A) Patch the resources the lifespan creates (preferred, minimal intrusion) B) Replace the whole lifespan function with a test double
A) Patch resources created during lifespan Idea: If your lifespan (before yield) creates clients/connections and stores them (e.g., on app.state or via dependency injection), patch those points so real external services aren’t touched.
Typical patterns 1) Patching attributes on app.state - In your lifespan, you might do:
- app.state.runtime = AgentRuntime(…)
- In tests, patch these with fakes before the TestClient initializes the app.
Example (pytest)
- snippet.python
import pytest from unittest.mock import Mock, AsyncMock, patch from fastapi.testclient import TestClient from dotigent.backend.app.main import app @pytest.fixture def client_with_fake_runtime(): fake_runtime = Mock() # If your routes call await runtime methods, use AsyncMock for those fake_runtime.run_agent = AsyncMock(return_value={"ok": True}) # Patch where the runtime is looked up in your code path. # If handlers access app.state.runtime: with patch.object(app.state, "runtime", fake_runtime, create=True): with TestClient(app) as client: yield client
2) Patching dependencies via Depends - If routes use dependency injection (FastAPI Depends), override them in tests:
- snippet.python
from fastapi import Depends # app dependency definition (in app code) def get_runtime(): return app.state.runtime @app.get("/agents") async def list_agents(runtime = Depends(get_runtime)): return await runtime.list_agents() # test override @pytest.fixture def client_override_dep(): fake_runtime = Mock() fake_runtime.list_agents = AsyncMock(return_value=[{"id": "a1"}]) from dotigent.backend.app.main import app, get_runtime app.dependency_overrides[get_runtime] = lambda: fake_runtime try: with TestClient(app) as client: yield client finally: app.dependency_overrides.clear()
3) Patching constructors used in lifespan - If lifespan does AgentRuntime(…) or ExternalClient(…), patch those constructors at the import location used by the lifespan:
- snippet.python
@patch("dotigent.backend.app.core.agent_runtime.AgentRuntime") def test_agents_with_patched_runtime(RuntimeMock): instance = RuntimeMock.return_value instance.list_agents = AsyncMock(return_value=[{"id":"a1"}]) from dotigent.backend.app.main import app with TestClient(app) as client: resp = client.get("/agents") assert resp.json() == [{"id":"a1"}]
B) Replace the entire lifespan Idea: If the app uses FastAPI(lifespan=…), you can provide a test lifespan that sets minimal fake resources, yielding immediately without real external setup.
Example: overriding lifespan with a test double
- snippet.python
from contextlib import asynccontextmanager from fastapi.testclient import TestClient from unittest.mock import Mock, AsyncMock from dotigent.backend.app.main import app @asynccontextmanager async def test_lifespan(_app): # Setup fake resources fake_runtime = Mock() fake_runtime.list_agents = AsyncMock(return_value=[{"id": "test"}]) _app.state.runtime = fake_runtime # Signal startup complete yield # Optionally cleanup _app.state.runtime = None def test_with_test_lifespan(monkeypatch): # monkeypatch the app.lifespan callable to our test_lifespan monkeypatch.setattr(app, "lifespan", test_lifespan) with TestClient(app) as client: r = client.get("/agents") assert r.status_code == 200 assert r.json() == [{"id":"test"}]
Notes and best practices - Choose patch point closest to consumption:
- If routes use app.state.runtime → patch app.state.runtime.
- If routes use Depends(getruntime) → use dependencyoverrides.
- If lifespan creates the resource via constructor → patch constructor at import path used by lifespan.
- Use AsyncMock for awaited methods to avoid “coroutine not awaited” issues. - Keep overrides localized: clear app.dependency_overrides after tests. - Avoid hitting real external systems in tests; patch network clients, databases, message buses, etc. - If you need to assert that startup code ran, assert the fake resource was set before your request (e.g., assert app.state.runtime is fake).
This mirrors the pattern around yield: your tests provide fakes during the suspended “app running” window, so handlers use them without invoking real services.