Parcourir la source

chore(backend-testing-01): create unit conftest with mocked dependencies

- mock_state_unit fixture with comprehensive state defaults
- mock_connection_unit fixture for hardware mocking
- app_with_mocked_state fixture for patched app testing
- async_client_with_mocked_state fixture for API testing
- cleanup_app_overrides fixture for dependency cleanup
tuanchris il y a 1 semaine
Parent
commit
aaa1d66ab0
1 fichiers modifiés avec 189 ajouts et 0 suppressions
  1. 189 0
      tests/unit/conftest.py

+ 189 - 0
tests/unit/conftest.py

@@ -0,0 +1,189 @@
+"""
+Unit test conftest.py - Fixtures specific to unit tests.
+
+Provides fixtures for mocking FastAPI dependencies and isolating tests
+from real state and hardware connections.
+"""
+import pytest
+from unittest.mock import MagicMock, AsyncMock, patch
+
+
+@pytest.fixture
+def mock_state_unit():
+    """Mock state for unit tests with common defaults.
+
+    This is a more comprehensive mock than the root conftest version,
+    specifically designed for API endpoint testing where we need to
+    control the application state precisely.
+    """
+    mock = MagicMock()
+
+    # Connection mock
+    mock.conn = MagicMock()
+    mock.conn.is_connected.return_value = False
+    mock.port = None
+    mock.is_connected = False
+    mock.preferred_port = None
+
+    # Pattern execution state
+    mock.current_playing_file = None
+    mock.is_running = False
+    mock.pause_requested = False
+    mock.stop_requested = False
+    mock.skip_requested = False
+    mock.execution_progress = None
+    mock.is_homing = False
+    mock.is_clearing = False
+
+    # Position state
+    mock.current_theta = 0.0
+    mock.current_rho = 0.0
+    mock.machine_x = 0.0
+    mock.machine_y = 0.0
+
+    # Speed and settings
+    mock.speed = 100
+    mock.clear_pattern_speed = None
+    mock.table_type = "dune_weaver"
+    mock.table_type_override = None
+    mock.homing = 0
+
+    # Playlist state
+    mock.current_playlist = None
+    mock.current_playlist_name = None
+    mock.current_playlist_index = None
+    mock.playlist_mode = None
+    mock.pause_time_remaining = 0
+    mock.original_pause_time = None
+
+    # LED state
+    mock.led_controller = None
+    mock.led_provider = "none"
+    mock.wled_ip = None
+    mock.hyperion_ip = None
+    mock.hyperion_port = 19444
+    mock.dw_led_num_leds = 60
+    mock.dw_led_gpio_pin = 18
+    mock.dw_led_pixel_order = "GRB"
+    mock.dw_led_brightness = 50
+    mock.dw_led_speed = 128
+    mock.dw_led_intensity = 128
+    mock.dw_led_idle_effect = "solid"
+    mock.dw_led_playing_effect = "rainbow"
+    mock.dw_led_idle_timeout_enabled = False
+    mock.dw_led_idle_timeout_minutes = 30
+    mock.dw_led_last_activity_time = 0
+
+    # Scheduled pause
+    mock.scheduled_pause_enabled = False
+    mock.scheduled_pause_time_slots = []
+    mock.scheduled_pause_control_wled = False
+    mock.scheduled_pause_finish_pattern = False
+    mock.scheduled_pause_timezone = None
+
+    # Steps and gear ratio
+    mock.x_steps_per_mm = 200.0
+    mock.y_steps_per_mm = 287.0
+    mock.gear_ratio = 10.0
+
+    # Auto-home settings
+    mock.auto_home_enabled = False
+    mock.auto_home_after_patterns = 10
+    mock.patterns_since_last_home = 0
+
+    # Custom clear patterns
+    mock.custom_clear_from_out = None
+    mock.custom_clear_from_in = None
+
+    # Homing offset
+    mock.angular_homing_offset_degrees = 0.0
+
+    # App settings
+    mock.app_name = "Dune Weaver"
+    mock.custom_logo_path = None
+
+    # MQTT settings
+    mock.mqtt_enabled = False
+    mock.mqtt_broker = None
+    mock.mqtt_port = 1883
+    mock.mqtt_username = None
+    mock.mqtt_password = None
+    mock.mqtt_topic_prefix = "dune_weaver"
+
+    # Table info
+    mock.table_id = None
+    mock.table_name = None
+    mock.known_tables = []
+
+    # Methods
+    mock.save = MagicMock()
+    mock.get_stop_event = MagicMock(return_value=None)
+    mock.get_skip_event = MagicMock(return_value=None)
+    mock.wait_for_interrupt = AsyncMock(return_value='timeout')
+    mock.pause_condition = MagicMock()
+    mock.pause_condition.__enter__ = MagicMock()
+    mock.pause_condition.__exit__ = MagicMock()
+    mock.pause_condition.notify_all = MagicMock()
+
+    return mock
+
+
+@pytest.fixture
+def mock_connection_unit():
+    """Mock connection for unit tests.
+
+    Provides a connection mock that simulates a connected device
+    without requiring actual hardware.
+    """
+    mock = MagicMock()
+    mock.is_connected.return_value = True
+    mock.send = MagicMock()
+    mock.readline = MagicMock(return_value="ok")
+    mock.in_waiting = MagicMock(return_value=0)
+    mock.flush = MagicMock()
+    mock.close = MagicMock()
+    mock.reset_input_buffer = MagicMock()
+    return mock
+
+
+@pytest.fixture
+def app_with_mocked_state(mock_state_unit):
+    """Fixture that patches state module before importing app.
+
+    This ensures the app uses mocked state for all operations.
+    Must be used before creating async_client.
+    """
+    with patch("modules.core.state.state", mock_state_unit):
+        with patch("modules.core.pattern_manager.state", mock_state_unit):
+            with patch("modules.core.playlist_manager.state", mock_state_unit):
+                with patch("modules.connection.connection_manager.state", mock_state_unit):
+                    from main import app
+                    yield app, mock_state_unit
+
+
+@pytest.fixture
+async def async_client_with_mocked_state(app_with_mocked_state):
+    """AsyncClient with mocked state for isolated API testing.
+
+    This fixture combines the app patching with the async client creation.
+    """
+    from httpx import ASGITransport, AsyncClient
+
+    app, mock_state = app_with_mocked_state
+
+    async with AsyncClient(
+        transport=ASGITransport(app=app),
+        base_url="http://test"
+    ) as client:
+        yield client, mock_state
+
+
+@pytest.fixture
+def cleanup_app_overrides():
+    """Fixture to ensure app.dependency_overrides is cleaned up after tests."""
+    from main import app
+
+    yield
+
+    # Cleanup after test
+    app.dependency_overrides.clear()