diff --git a/features/environment.py b/features/environment.py index 991d4b266..48d016886 100644 --- a/features/environment.py +++ b/features/environment.py @@ -824,3 +824,11 @@ def after_scenario(context, scenario): # Scenario.run() wrapper installed in _install_tdd_expected_fail_patch(), # NOT in this hook. See before_all() and CONTRIBUTING.md > TDD Issue # Test Tags for the full specification. + + # Clean up test UnitOfWork fixtures if any were created + try: + from features.mocks.test_uow_factory import cleanup_test_uow + + cleanup_test_uow(context) + except ImportError: + pass diff --git a/features/mocks/test_uow_factory.py b/features/mocks/test_uow_factory.py index f4eef5b02..921300d66 100644 --- a/features/mocks/test_uow_factory.py +++ b/features/mocks/test_uow_factory.py @@ -9,13 +9,22 @@ both test suites exercise the same database setup. Any changes to ``UnitOfWork.__init__`` must be reflected here in a single place rather than in each consuming test file. +Behave Context Adapters: +- ``use_test_uow(context)`` — Attach a test UoW to a Behave context and + register engine cleanup on scenario teardown. + Used by: - ``features/steps/plan_use_action_args_integrity_steps.py`` +- ``features/steps/plan_service_steps.py`` +- ``features/steps/project_cli_steps.py`` +- ``features/steps/project_service_coverage_boost_steps.py`` - ``robot/helper_plan_use_action_args_integrity.py`` """ from __future__ import annotations +import contextlib +from collections.abc import Callable from typing import Any from sqlalchemy import create_engine, event @@ -69,3 +78,77 @@ def build_test_uow() -> tuple[UnitOfWork, sessionmaker[Session], Engine]: uow._require_confirmation = False return uow, sf, engine + + +def use_test_uow( + context: Any, + seed_hook: Callable[[UnitOfWork], None] | None = None, +) -> UnitOfWork: + """Attach a test UnitOfWork to a Behave context with automatic cleanup. + + This is the recommended way to set up database fixtures in Behave step + definitions. It ensures: + - Consistent in-memory SQLite configuration across all tests + - Foreign key PRAGMA enforcement + - Automatic engine cleanup on scenario teardown + - Optional seed hook for test data setup + + Args: + context: The Behave context object (typically from a step function). + seed_hook: Optional callable that receives the UoW and populates + test data. Called immediately after UoW creation. + + Returns: + The configured UnitOfWork instance, also attached to context.uow. + + Example: + @given("a plan service with test database") + def step_plan_service_with_test_db(context): + uow = use_test_uow(context) + context.plan_service = PlanService(uow) + """ + uow, sf, engine = build_test_uow() + + # Attach to context for use in step definitions + context.uow = uow + context.session_factory = sf + context.engine = engine + + # Register cleanup callback + def cleanup_engine() -> None: + """Dispose of the engine connection pool.""" + engine.dispose() + + # Store cleanup function for later invocation + if not hasattr(context, "_cleanup_callbacks"): + context._cleanup_callbacks = [] + context._cleanup_callbacks.append(cleanup_engine) + + # Run optional seed hook + if seed_hook is not None: + seed_hook(uow) + + return uow + + +def cleanup_test_uow(context: Any) -> None: + """Clean up test UnitOfWork resources attached to a Behave context. + + This should be called in an ``after_scenario`` hook or similar teardown. + + Args: + context: The Behave context object. + """ + if hasattr(context, "_cleanup_callbacks"): + for callback in context._cleanup_callbacks: + with contextlib.suppress(Exception): + callback() + context._cleanup_callbacks.clear() + + # Clear references + if hasattr(context, "uow"): + delattr(context, "uow") + if hasattr(context, "session_factory"): + delattr(context, "session_factory") + if hasattr(context, "engine"): + delattr(context, "engine")