diff --git a/tests/conftest.py b/tests/conftest.py index 349a412..28bdce4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,8 @@ -from collections.abc import AsyncIterator +from collections.abc import AsyncIterator, Iterator import pytest import pytest_asyncio +import structlog from asgi_lifespan import LifespanManager from fastapi import FastAPI from httpx import ASGITransport, AsyncClient @@ -10,6 +11,25 @@ from src.config import Settings from src.main import create_app +@pytest.fixture(autouse=True) +def _reset_structlog_after_test() -> Iterator[None]: + """Undo any real `configure_logging()` call before the next test runs. + + Any test that exercises the app's lifespan (directly, or via the `client`/ + `api_client` fixtures below and in `test_domains_api.py`) calls the real + `configure_logging()`, which mutates *global* structlog/stdlib state -- + including `cache_logger_on_first_use=True`. Left in place, that setting + silently breaks `structlog.testing.capture_logs()` in unrelated tests + later in the same pytest process: a module-level + `logger = structlog.get_logger(__name__)` cached under the real config no + longer routes through `capture_logs()`'s temporary processor swap, so + assertions on captured events see nothing (ADR-0016: isolate per test -- + this generalizes to global config mutations, not just data). + """ + yield + structlog.reset_defaults() + + @pytest.fixture def settings() -> Settings: # Every external dependency points at a closed port so unit tests never @@ -36,6 +56,24 @@ def app(settings: Settings) -> FastAPI: return create_app(settings) +@pytest.fixture(autouse=True) +def _no_real_logging_configuration(monkeypatch: pytest.MonkeyPatch) -> None: + """Prevent the app lifespan from calling the real `configure_logging()`. + + It sets `cache_logger_on_first_use=True` (ADR-0011), which permanently + monkeypatches the `.bind` method on whichever module-level + `logger = structlog.get_logger(__name__)` instance is used first -- + `structlog.reset_defaults()` only resets *global* config, not that + per-instance mutation, so real configuration leaking into one test would + silently break `structlog.testing.capture_logs()` in every test that runs + afterward in the same process (ADR-0016: isolate per test). Tests that + spin up the full app via `LifespanManager` (`client`, `api_client`) are + testing HTTP behavior, not logging output, so they don't need it for + real. + """ + monkeypatch.setattr("src.bootstrap.lifespan.configure_logging", lambda *a, **k: None) + + @pytest_asyncio.fixture async def client(app: FastAPI) -> AsyncIterator[AsyncClient]: async with (