test: pilot core database stub helper (#3685)

This commit is contained in:
Alexandre Teixeira
2026-06-09 21:23:33 +01:00
committed by GitHub
parent b1af29c7bc
commit a22c0fa85e
6 changed files with 169 additions and 27 deletions
+15 -4
View File
@@ -150,15 +150,26 @@ Use for the repeated file-backed temp sqlite setup in tests.
under test reads, and must keep the returned objects alive.
- Do not use it as a general DB fixture framework.
### `tests.helpers.db_stubs.make_core_db_stub`
Use for small import-time `core.database` stubs with a placeholder
`SessionLocal`.
- Pass model names via `models` when MagicMock attributes are sufficient.
- Pass `attributes` when an import needs exact placeholder values.
- Set `install_core_package=True` only when the test also needs a fake parent
`core` module stub.
- Keep custom fake sessions and route-specific database behavior local.
## What not to abstract yet
Some remaining patterns should stay as-is for now rather than being forced into
helpers:
- Large mixed files such as security/review regression files.
- Setup-oriented `sys.modules` stub installers.
- Broad setup-oriented `sys.modules` stub installers.
- One-off custom module patching.
- DB/session/route setup, until it has been audited separately.
- Custom DB session, route, and app setup.
## Validation expectations
@@ -178,7 +189,7 @@ Run validation locally before opening or approving a PR. Practical checks:
1. Import-state cleanup - complete.
2. Document helper conventions (this file).
3. Audit fake DB / `SessionLocal` / route setup duplication.
4. Add tiny helpers only when the repeated semantics are clear.
3. Pilot the repeated import-time `core.database` stub helper.
4. Add further tiny helpers only when the repeated semantics are clear.
5. Start low-risk file moves only after helper conventions are documented.
6. Avoid moving high-risk security/route regression files first.
+15 -2
View File
@@ -4,17 +4,30 @@ import types
from unittest.mock import MagicMock
def make_core_db_stub(monkeypatch, models=()):
def make_core_db_stub(
monkeypatch,
models=(),
*,
attributes=None,
install_core_package=False,
):
"""Create a core.database stub and inject it via monkeypatch.
Always sets SessionLocal. Pass model class names via `models` to set
each as a MagicMock attribute on the stub.
each as a MagicMock attribute on the stub. Pass `attributes` to override
specific values, and `install_core_package` when the import also needs a
stub parent package.
Returns the stub module for optional further configuration.
"""
if install_core_package:
monkeypatch.setitem(sys.modules, "core", types.ModuleType("core"))
db = types.ModuleType("core.database")
db.SessionLocal = MagicMock()
for name in models:
setattr(db, name, MagicMock())
for name, value in (attributes or {}).items():
setattr(db, name, value)
monkeypatch.setitem(sys.modules, "core.database", db)
return db
+121
View File
@@ -0,0 +1,121 @@
import sys
from contextlib import contextmanager
from types import ModuleType
from unittest.mock import MagicMock
from pytest import MonkeyPatch
from tests.helpers.db_stubs import make_core_db_stub
_MISSING = object()
_MODULE_NAMES = ("core", "core.database")
@contextmanager
def _preserve_core_modules():
original_modules = {
name: sys.modules.get(name, _MISSING) for name in _MODULE_NAMES
}
try:
yield
finally:
for name in _MODULE_NAMES:
sys.modules.pop(name, None)
for name, module in original_modules.items():
if module is not _MISSING:
sys.modules[name] = module
def test_models_create_mock_attributes(monkeypatch):
db = make_core_db_stub(monkeypatch, models=("User", "Session"))
assert sys.modules["core.database"] is db
assert isinstance(db.SessionLocal, MagicMock)
assert isinstance(db.User, MagicMock)
assert isinstance(db.Session, MagicMock)
def test_attributes_override_defaults_and_model_mocks(monkeypatch):
session_local = object()
email_account = object()
db = make_core_db_stub(
monkeypatch,
models=("EmailAccount",),
attributes={
"SessionLocal": session_local,
"EmailAccount": email_account,
},
)
assert db.SessionLocal is session_local
assert db.EmailAccount is email_account
def test_core_module_installation_is_opt_in():
with _preserve_core_modules():
sys.modules.pop("core", None)
sys.modules.pop("core.database", None)
monkeypatch = MonkeyPatch()
try:
db = make_core_db_stub(monkeypatch)
assert "core" not in sys.modules
assert sys.modules["core.database"] is db
finally:
monkeypatch.undo()
def test_existing_core_is_preserved_when_installation_is_disabled():
with _preserve_core_modules():
original_core = ModuleType("core")
sys.modules["core"] = original_core
sys.modules.pop("core.database", None)
monkeypatch = MonkeyPatch()
try:
db = make_core_db_stub(monkeypatch, install_core_package=False)
assert sys.modules["core"] is original_core
assert sys.modules["core.database"] is db
finally:
monkeypatch.undo()
assert sys.modules["core"] is original_core
assert "core.database" not in sys.modules
def test_undo_removes_modules_that_were_absent():
with _preserve_core_modules():
sys.modules.pop("core", None)
sys.modules.pop("core.database", None)
monkeypatch = MonkeyPatch()
try:
make_core_db_stub(monkeypatch, install_core_package=True)
assert "core" in sys.modules
assert "core.database" in sys.modules
finally:
monkeypatch.undo()
assert "core" not in sys.modules
assert "core.database" not in sys.modules
def test_undo_restores_existing_modules():
with _preserve_core_modules():
original_core = ModuleType("core")
original_database = ModuleType("core.database")
sys.modules["core"] = original_core
sys.modules["core.database"] = original_database
monkeypatch = MonkeyPatch()
try:
make_core_db_stub(monkeypatch, install_core_package=True)
assert sys.modules["core"] is not original_core
assert sys.modules["core.database"] is not original_database
finally:
monkeypatch.undo()
assert sys.modules["core"] is original_core
assert sys.modules["core.database"] is original_database
+6 -6
View File
@@ -4,6 +4,7 @@ from types import ModuleType, SimpleNamespace
import pytest
from tests.helpers.cli_loader import load_script
from tests.helpers.db_stubs import make_core_db_stub
class _Conn:
@@ -37,14 +38,13 @@ def _load_mail_cli(monkeypatch):
pollers = ModuleType("routes.email_pollers")
pollers._scheduled_poll_once = lambda: {}
pollers._run_auto_summarize_once = lambda **kwargs: ""
core_mod = ModuleType("core")
database_mod = ModuleType("core.database")
database_mod.SessionLocal = object
database_mod.EmailAccount = object
monkeypatch.setitem(sys.modules, "routes.email_helpers", helpers)
monkeypatch.setitem(sys.modules, "routes.email_pollers", pollers)
monkeypatch.setitem(sys.modules, "core", core_mod)
monkeypatch.setitem(sys.modules, "core.database", database_mod)
make_core_db_stub(
monkeypatch,
attributes={"SessionLocal": object, "EmailAccount": object},
install_core_package=True,
)
return load_script("odysseus-mail")
+6 -7
View File
@@ -2,6 +2,7 @@ import sys
from types import ModuleType
from tests.helpers.cli_loader import load_script
from tests.helpers.db_stubs import make_core_db_stub
def _load_mail_cli(monkeypatch):
@@ -17,15 +18,13 @@ def _load_mail_cli(monkeypatch):
pollers._scheduled_poll_once = lambda: {}
pollers._run_auto_summarize_once = lambda **kwargs: ""
core_mod = ModuleType("core")
database_mod = ModuleType("core.database")
database_mod.SessionLocal = object
database_mod.EmailAccount = object
monkeypatch.setitem(sys.modules, "routes.email_helpers", helpers)
monkeypatch.setitem(sys.modules, "routes.email_pollers", pollers)
monkeypatch.setitem(sys.modules, "core", core_mod)
monkeypatch.setitem(sys.modules, "core.database", database_mod)
make_core_db_stub(
monkeypatch,
attributes={"SessionLocal": object, "EmailAccount": object},
install_core_package=True,
)
return load_script("odysseus-mail")
+6 -8
View File
@@ -1,17 +1,15 @@
import sys
from types import ModuleType
from types import SimpleNamespace
from tests.helpers.cli_loader import load_script
from tests.helpers.db_stubs import make_core_db_stub
def _load_sessions_cli(monkeypatch):
core_mod = ModuleType("core")
database_mod = ModuleType("core.database")
database_mod.SessionLocal = object
database_mod.Session = object
monkeypatch.setitem(sys.modules, "core", core_mod)
monkeypatch.setitem(sys.modules, "core.database", database_mod)
make_core_db_stub(
monkeypatch,
attributes={"SessionLocal": object, "Session": object},
install_core_package=True,
)
return load_script("odysseus-sessions")