Repository navigation
refactor(tests): extract shared isolated_module_loader for backend-module unit tests #803
Description
Summary
Four unit test files load real backend modules in isolation by stubbing heavy deps in sys.modules at import time, then spec_from_file_location + exec_module against the real source file. The plumbing is ~50 lines of near-identical boilerplate per file:
tests/unit/test_telegram_webhook_backfill.pytests/unit/test_inject_assigned_credentials.pytests/unit/test_subscription_auto_switch_no_cred_import.pytests/unit/test_cleanup_unreachable_orphan.py
Each one independently rediscovers (or copies from a neighbor):
- Which slots to stub in
sys.modulesto keep the target import side-effect-free. - How to spec-load the module under test from its file path.
- How to snapshot/restore
sys.modulesso the stubs don't leak across the pytest session (the_STUBBED_MODULE_NAMES+_restore_sys_modulesescape hatch fromtests/lint_sys_modules.py).
This is the technique that needs to exist for any test of backend code that depends on heavy modules the unit venv doesn't carry (docker, the full database.py stack, …) — but every new such test pays the discoverability + boilerplate tax.
Proposed design
Add tests/testing_utils/isolated_module_loader.py with one entry point:
```python
def load_isolated(
module_source_path: Path,
*,
stubs: dict[str, ModuleType | Mock],
snapshot_names: list[str],
module_name: str | None = None,
) -> ModuleType:
"""Spec-load a backend module by file path with sys.modules stubs.
Stubs are planted under the names in `stubs.keys()`, then the target
module is exec'd. The function returns the loaded module. Cleanup
happens via a pytest-plugin-registered autouse fixture: any test
file that imports this module gets sys.modules snapshot/restore
automatically for the union of all `stubs.keys()` and `snapshot_names`.
\"\"\"
```
A pytest plugin in tests/testing_utils/conftest.py (or registered via pytest_plugins) auto-applies the snapshot/restore fixture for any test file that imports load_isolated. This eliminates the need for each test to declare _STUBBED_MODULE_NAMES + _restore_sys_modules by hand.
Migration plan
- Land the helper + plugin with green tests against an empty consumer (smoke test only).
- Migrate each of the four files in a separate small PR — keep the diff per file to ~30 lines net (delete 50 lines of plumbing, add ~20 lines of helper call + assertions).
- After all four migrate cleanly, tighten
tests/lint_sys_modules.py: ban bare_STUBBED_MODULE_NAMESdeclarations OUTSIDE the helper itself; the helper becomes the only path to the escape hatch.
Why this matters
- Discoverability: the named-helper escape hatch is documented in the lint script's docstring but not in any user-facing test file or guide. PRs fix(cleanup): force-fail orphan when agent unreachable during re-verify (#497) #783 and bug: auto-switch (SUB-003) marks triggering execution failed due to spurious .credentials.enc injection after container restart #606 both hit the lint, dug through the script source, and re-invented the same pattern. A first-class helper makes the pattern visible.
- Maintainability: a structural change to the snapshot/restore semantics (e.g. add request-id tagging, change cleanup order) propagates from one place instead of four near-identical patches.
- Future call sites: every test that needs to isolate a backend module against a thin unit venv will hit this. Cost-of-next-test drops from "copy 50 lines from a neighbor and pray" to "one import + one function call."
Out of scope
This issue does NOT propose removing the named-helper escape hatch from the lint script. The hatch remains the documented exit; the helper just makes it the obvious one.
Refs
- fix(tests): test_audit_log_unit.py — 50 collection ImportErrors when run in full suite #762 — origin of the lint
- fix(tests): conftest sys.modules baseline + tests-side lint (closes #762) #791 — lint + baseline introduction
- bug: auto-switch (SUB-003) marks triggering execution failed due to spurious .credentials.enc injection after container restart #606 / fix(cleanup): force-fail orphan when agent unreachable during re-verify (#497) #783 — recent files that re-invented the pattern
Activity