Skip to content

Latest commit

 

History

History
615 lines (471 loc) · 20.2 KB

File metadata and controls

615 lines (471 loc) · 20.2 KB

Unit Tests Documentation

This document describes the unit test structure, organization, and coverage for the Adaptive Cover Pro integration.

Overview

The test suite provides comprehensive coverage of the core calculation logic and helper functions. Tests are organized by module and use pytest with extensive fixtures for mocking Home Assistant dependencies.

Current Status:

  • Total Tests: 178
  • Overall Coverage: 28% (due to platform files not yet tested)
  • calculation.py Coverage: 91% (primary target achieved)

Test Organization

Test Files

tests/
├── conftest.py                 # Shared fixtures and configuration
├── test_calculation.py         # Calculation logic (135 tests, 91% coverage)
├── test_helpers.py            # Helper functions (29 tests, 100% coverage)
└── test_inverse_state.py      # Inverse state behavior (14 tests, 100% coverage)

Test Markers

Tests use pytest markers to categorize:

  • @pytest.mark.unit - Unit tests (fast, no I/O)
  • @pytest.mark.integration - Integration tests (slower, may involve I/O)
  • @pytest.mark.asyncio - Async tests requiring event loop

Test Coverage by Module

calculation.py (91% coverage, 135 tests)

Phase 1: AdaptiveGeneralCover Properties (46 tests)

Tests for base cover class properties used by all cover types:

Azimuth Calculations:

  • test_azi_min_abs_standard - Standard min azimuth calculation (180 - 45 = 135)
  • test_azi_max_abs_standard - Standard max azimuth calculation (180 + 45 = 225)
  • test_azi_min_abs_wrapping_around_zero - Min azimuth wraps around 0° (10 - 45 = 325)
  • test_azi_max_abs_wrapping_around_360 - Max azimuth wraps around 360° (350 + 45 = 35)
  • test_azi_edges_calculation - FOV edge calculation (fov_left + fov_right)

Gamma Angle Calculations:

  • test_gamma_sun_directly_in_front - Gamma = 0° when sun directly ahead
  • test_gamma_sun_to_left - Positive gamma for sun to the left
  • test_gamma_sun_to_right - Negative gamma for sun to the right
  • test_gamma_wrapping_around_180 - Gamma wrapping at ±180° boundaries
  • test_gamma_wrapping_negative - Gamma wrapping in negative direction

Elevation Validation:

  • test_valid_elevation_with_both_limits - Elevation within min/max range
  • test_valid_elevation_below_minimum - Elevation below min threshold
  • test_valid_elevation_above_maximum - Elevation above max threshold
  • test_valid_elevation_only_min_set - Only minimum elevation configured
  • test_valid_elevation_only_max_set - Only maximum elevation configured
  • test_valid_elevation_neither_set_above_horizon - Default behavior (>= 0°)
  • test_valid_elevation_neither_set_below_horizon - Below horizon when no limits

Sun Validity:

  • test_valid_sun_in_fov_and_above_horizon - Sun valid when in FOV and above horizon
  • test_valid_sun_outside_left_fov - Sun invalid outside left FOV boundary
  • test_valid_sun_outside_right_fov - Sun invalid outside right FOV boundary
  • test_valid_sun_below_horizon - Sun invalid when below horizon
  • test_valid_sun_at_left_boundary - Boundary condition at left edge
  • test_valid_sun_at_right_boundary - Boundary condition at right edge

Blind Spot Detection:

  • test_is_sun_in_blind_spot_true - Sun in blind spot area with valid elevation
  • test_is_sun_in_blind_spot_elevation_too_high - Elevation above blind spot threshold
  • test_is_sun_in_blind_spot_outside_area - Sun outside blind spot azimuth range
  • test_is_sun_in_blind_spot_disabled - Blind spot detection disabled
  • test_is_sun_in_blind_spot_none_values - Blind spot with None configuration

Direct Sun Validity (6 tests):

  • test_direct_sun_valid_all_conditions_met - All conditions met: in FOV, above horizon, before sunset, no blind spot
  • test_direct_sun_valid_after_sunset - Returns False after sunset (reproduces End Sun time bug scenario)
  • test_direct_sun_valid_in_blind_spot - Returns False when sun in blind spot (even if in FOV)
  • test_direct_sun_valid_outside_fov - Returns False when sun outside field of view
  • test_direct_sun_valid_before_sunrise - Returns False before sunrise time
  • test_direct_sun_valid_with_sunset_offset - Respects sunset offset configuration

Default Position Logic:

  • test_default_position_before_sunset - Returns h_def before sunset
  • test_default_position_after_sunset - Returns sunset_pos after sunset
  • test_fov_method_returns_list - FOV method returns [azi_min_abs, azi_max_abs]

Phase 2: Cover Type Classes (50 tests)

AdaptiveVerticalCover (8 tests):

  • test_calculate_position_standard - Standard blind height calculation (45° sun)
  • test_calculate_position_high_sun - High sun clips to window height
  • test_calculate_position_low_sun - Low sun creates shorter blind height
  • test_calculate_position_with_gamma_angle - Angled sun increases path length
  • test_calculate_position_clips_to_window_height - Maximum clipping
  • test_calculate_percentage_standard - Height to percentage conversion
  • test_calculate_percentage_with_different_window_height - Window height variations
  • test_calculate_percentage_with_different_distance - Distance variations

AdaptiveHorizontalCover (7 tests):

  • test_calculate_position_standard - Standard awning extension
  • test_calculate_position_with_awning_angle - Non-zero awning angle
  • test_calculate_position_high_sun - High sun minimal shadow
  • test_calculate_position_low_sun - Low sun longer shadow
  • test_calculate_percentage_standard - Awning percentage conversion
  • test_calculate_percentage_with_different_awning_length - Length variations
  • test_awning_angle_variations - Multiple angle scenarios (0°, 15°, 30°, 45°)

AdaptiveTiltCover (9 tests):

  • test_beta_property - Beta angle calculation in radians
  • test_calculate_position_mode1 - Tilt angle for mode1 (90° max)
  • test_calculate_position_mode2 - Tilt angle for mode2 (180° max)
  • test_calculate_percentage_mode1 - Percentage conversion mode1
  • test_calculate_percentage_mode2 - Percentage conversion mode2
  • test_slat_depth_variations - Various slat depths
  • test_slat_distance_variations - Various slat distances
  • test_beta_with_different_sun_angles - Beta at different elevations
  • test_position_with_gamma_angle - Tilt with angled sun

Phase 3: NormalCoverState (20 tests)

State determination logic for normal (non-climate) operation:

  • test_get_state_sun_valid - Uses calculated position when sun valid
  • test_get_state_sun_invalid - Uses default position when sun invalid
  • test_get_state_after_sunset - Uses sunset_pos after sunset
  • test_max_position_clamping - Max position limiting
  • test_min_position_clamping - Min position limiting
  • test_min_position_with_bool_flag_sun_valid - Min with direct_sun_valid flag
  • test_max_position_with_bool_flag_sun_valid - Max with direct_sun_valid flag
  • test_clipping_to_100 - Position clips to valid range
  • test_combined_min_max_clamping - Both min and max applied

Phase 4: ClimateCoverData (40 tests)

Climate data property tests covering all entity types and edge cases:

Temperature Properties (6 tests):

  • test_outside_temperature_from_outside_entity - Reading from outside sensor
  • test_outside_temperature_from_weather_entity - Reading from weather entity
  • test_inside_temperature_from_sensor - Reading from temperature sensor
  • test_inside_temperature_from_climate_entity - Reading from climate entity
  • test_get_current_temperature_outside - temp_switch=True (outside)
  • test_get_current_temperature_inside - temp_switch=False (inside)

Presence Detection (7 tests):

  • test_is_presence_device_tracker_home - device_tracker at "home"
  • test_is_presence_device_tracker_away - device_tracker away
  • test_is_presence_zone_occupied - zone with people (count > 0)
  • test_is_presence_zone_empty - zone empty (count = 0)
  • test_is_presence_binary_sensor_on - binary_sensor "on"
  • test_is_presence_binary_sensor_off - binary_sensor "off"
  • test_is_presence_none_entity - No entity defaults to True

Winter/Summer Detection (3 tests):

  • test_is_winter_true - Temperature below temp_low threshold
  • test_is_winter_false - Temperature above temp_low threshold
  • test_is_summer_true - Temperature above temp_high and outside_high
  • test_is_summer_false - Temperature below thresholds
  • test_outside_high_true - Outside temp above threshold

Weather & Light (5 tests):

  • test_is_sunny_true - Weather state in sunny conditions list
  • test_is_sunny_false - Weather state not in list
  • test_lux_below_threshold - Lux value below threshold
  • test_lux_above_threshold - Lux value above threshold
  • test_lux_disabled - Lux checking disabled (_use_lux=False)
  • test_irradiance_below_threshold - Irradiance below threshold
  • test_irradiance_disabled - Irradiance checking disabled

Phase 5: ClimateCoverState (50 tests)

Complex climate-aware state logic with winter/summer strategies:

Normal Type Cover (10 tests):

  • test_normal_type_cover_with_presence - Delegates to normal_with_presence
  • test_normal_type_cover_without_presence - Delegates to normal_without_presence
  • test_normal_with_presence_winter_sun_valid - Winter: open fully (100%)
  • test_normal_with_presence_not_sunny - Not sunny: use default
  • test_normal_with_presence_summer_transparent - Summer + transparent: 0%
  • test_normal_with_presence_intermediate - Intermediate: calculated position
  • test_normal_without_presence_summer - Summer no presence: close (0%)
  • test_normal_without_presence_winter - Winter no presence: open (100%)
  • test_normal_without_presence_default - Default path when sun not valid

Tilt State (2 tests):

  • test_tilt_state_mode1 - Tilt state calculation for mode1 (90°)
  • test_tilt_state_mode2 - Tilt state calculation for mode2 (180°)

State Integration (4 tests):

  • test_get_state_blind_type - Routes to normal_type_cover for blinds
  • test_get_state_tilt_type - Routes to tilt_state for tilt covers
  • test_get_state_max_position_clamping - Max position applied in climate state
  • test_get_state_min_position_clamping - Min position applied in climate state

helpers.py (100% coverage, 29 tests)

Helper function tests covering all utility functions:

get_safe_state (8 tests):

  • Valid numeric states (integers, floats)
  • Invalid states (None, unavailable, unknown)
  • String to float conversion
  • Entity not found handling

get_domain (4 tests):

  • Extracts domain from entity_id
  • Handles various entity formats
  • None handling

get_state_attr (4 tests):

  • Retrieves attributes from entities
  • Missing attribute handling
  • None entity handling

get_position (5 tests):

  • Cover position attribute reading
  • Position-capable vs open/close-only covers
  • None handling

get_open_close_state (4 tests):

  • Maps cover states to percentages
  • open → 100, closed → 0
  • Invalid state handling

inverse_state.py (100% coverage, 14 tests)

Inverse state behavior tests (critical feature):

Function Tests (5 tests):

  • Inverts 0 → 100
  • Inverts 100 → 0
  • Inverts 50 → 50
  • Inverts intermediate values correctly

Integration Tests (9 tests):

  • Position-capable flow with inversion
  • Open/close flow above/at/below threshold
  • Order of operations (invert → threshold check)
  • Disabled with interpolation

Running Tests

All Tests

# Run all tests with coverage
pytest tests/ -v --cov=custom_components/adaptive_cover_pro --cov-report=term

# Run all tests with detailed output
pytest tests/ -v --tb=short

Specific Test Files

# Run only calculation tests
pytest tests/test_calculation.py -v

# Run only helper tests
pytest tests/test_helpers.py -v

# Run only inverse state tests
pytest tests/test_inverse_state.py -v

Coverage Reports

# Generate HTML coverage report
pytest tests/ --cov=custom_components/adaptive_cover_pro --cov-report=html

# View report
open htmlcov/index.html

Running in Virtual Environment

# Activate venv
source venv/bin/activate

# Run tests
pytest tests/ -v

Fixtures

Fixtures are defined in tests/conftest.py and provide reusable test components.

Core Fixtures

hass

  • Mock HomeAssistant instance
  • Configured with default units (°C)
  • Returns None for states.get() by default

mock_logger

  • Mock ConfigContextAdapter logger
  • All logging methods mocked (debug, info, warning, error)

mock_sun_data

  • Mock SunData instance
  • Default values: azimuth=180°, elevation=45°
  • Predictable sun position for testing

mock_state

  • Factory fixture for creating mock state objects
  • Usage: mock_state("entity_id", "state_value", {"attr": "value"})

Configuration Fixtures

sample_vertical_config

  • Standard vertical cover configuration dictionary
  • Window facing south (180°), 45° FOV each side
  • Distance 0.5m, window height 2.0m

sample_horizontal_config

  • Standard horizontal cover configuration
  • Awning length 2.0m, angle 0°

sample_tilt_config

  • Standard tilt cover configuration
  • Slat depth 0.02m, distance 0.03m, mode1

sample_climate_config

  • Standard climate mode configuration
  • Temperature thresholds, weather conditions

Cover Instance Fixtures

vertical_cover_instance

  • Real AdaptiveVerticalCover instance
  • Fully instantiated with all parameters
  • Ready for method testing

horizontal_cover_instance

  • Real AdaptiveHorizontalCover instance
  • Includes awning-specific parameters

tilt_cover_instance

  • Real AdaptiveTiltCover instance
  • Includes slat parameters and mode

climate_data_instance

  • Real ClimateCoverData instance
  • Mocked entity states for temperature, presence

Test Patterns

Testing Properties

@pytest.mark.unit
def test_property_name(self, vertical_cover_instance):
    """Test property with standard configuration."""
    # Modify instance if needed
    vertical_cover_instance.sol_azi = 180.0

    # Test property
    result = vertical_cover_instance.property_name

    # Assert expectations
    assert result == expected_value

Testing Methods

@pytest.mark.unit
def test_method_name(self, vertical_cover_instance):
    """Test method with specific inputs."""
    # Setup
    vertical_cover_instance.some_param = test_value

    # Execute
    result = vertical_cover_instance.method_name()

    # Assert
    assert result == expected_result

Mocking Datetime

@pytest.mark.unit
@patch("custom_components.adaptive_cover_pro.calculation.datetime")
def test_with_datetime(self, mock_datetime, vertical_cover_instance):
    """Test with mocked datetime."""
    # Mock current time
    mock_datetime.utcnow.return_value = datetime(2024, 1, 1, 12, 0, 0)

    # Mock sunset/sunrise
    vertical_cover_instance.sun_data.sunset = MagicMock(
        return_value=datetime(2024, 1, 1, 18, 0, 0)
    )
    vertical_cover_instance.sun_data.sunrise = MagicMock(
        return_value=datetime(2024, 1, 1, 6, 0, 0)
    )

    # Test code
    result = vertical_cover_instance.sunset_valid
    assert result is False

Mocking Entity States

@pytest.mark.unit
def test_with_entity_state(self, hass, mock_state):
    """Test with mocked entity state."""
    # Create mock state
    temp_state = mock_state("sensor.temperature", "22.5", {})

    # Configure hass to return it
    hass.states.get.return_value = temp_state

    # Test code that uses hass.states.get()

Testing Edge Cases

@pytest.mark.unit
def test_edge_case_nan_handling(self, tilt_cover_instance):
    """Test edge case where calculation produces NaN."""
    try:
        # Execute
        result = tilt_cover_instance.calculate_percentage()

        # If no error, assert valid range
        assert 0 <= result <= 100
    except ValueError:
        # ValueError from round(NaN) is expected
        pass

Coverage Goals

Current Coverage

Module Statements Covered Coverage Status
calculation.py 306 278 91% ✅ Target achieved
helpers.py 47 47 100% ✅ Complete
inverse_state behavior - - 100% ✅ Complete
coordinator.py 543 121 22% 🔄 Future work
Platform files ~900 0 0% 🔄 Future work

Missing Coverage in calculation.py (9%)

Lines 53-70: solar_times() method

  • Requires SunData with real pandas DataFrames
  • Integration-level test needed
  • Not critical for unit test coverage

Lines 313, 372, 382, 399: ClimateCoverData property edge cases

  • None handling paths
  • Require specific entity configurations

Lines 448-451, 456-465, 474, 480, 482-487, 489-494: ClimateCoverState edge cases

  • Complex branching in climate logic
  • Some paths require specific combinations

Best Practices

When Writing New Tests

  1. Use descriptive test names

    • Format: test_<what>_<condition>_<expected>
    • Example: test_gamma_sun_to_left clearly describes scenario
  2. One assertion per test when possible

    • Makes failures easier to diagnose
    • Exception: Related assertions (e.g., type and range)
  3. Test edge cases explicitly

    • Boundary values (0, 100, min, max)
    • None/invalid inputs
    • Wrapping/overflow scenarios
  4. Mock external dependencies

    • Always mock datetime for time-dependent tests
    • Mock entity states via fixtures
    • Don't make real I/O calls
  5. Use fixtures for common setup

    • Add to conftest.py if used by multiple tests
    • Keep fixtures focused and simple
  6. Document complex test scenarios

    • Explain non-obvious setup
    • Document expected behavior
    • Include calculation examples in comments

Code Coverage Guidelines

  • 90%+ coverage for core calculation logic (✅ achieved)
  • 100% coverage for critical utility functions (✅ achieved)
  • 100% coverage for critical behaviors like inverse_state (✅ achieved)
  • Future: Add integration tests for coordinator and platforms

Continuous Integration

Tests run automatically on:

  • Every commit via GitHub Actions
  • Pull requests before merge
  • Coverage reports uploaded to workflow artifacts

CI Configuration: .github/workflows/test.yml

Python Versions Tested:

  • Python 3.11
  • Python 3.12

Home Assistant Versions:

  • Minimum: 2024.5.0
  • Tested with latest stable

Troubleshooting

Common Test Failures

ImportError: No module named 'pytest'

# Activate virtual environment first
source venv/bin/activate
pip install -e ".[dev]"

datetime comparison errors

# Always mock datetime for time-dependent tests
@patch("custom_components.adaptive_cover_pro.calculation.datetime")
def test_with_time(self, mock_datetime, ...):
    mock_datetime.utcnow.return_value = datetime(2024, 1, 1, 12, 0, 0)

NaN comparison failures

# Use np.isnan() or handle ValueError from round(NaN)
assert (0 <= result <= 100) or np.isnan(result)

numpy.int64 vs int type errors

# Accept both types
assert isinstance(result, (int, np.integer))

Running Specific Tests

# Run single test
pytest tests/test_calculation.py::test_gamma_angle_calculation_sun_directly_in_front -v

# Run test class
pytest tests/test_calculation.py::TestAdaptiveVerticalCover -v

# Run tests matching pattern
pytest tests/ -k "blind_spot" -v

Future Test Expansion

Priority Areas

  1. Coordinator Tests

    • State management
    • Entity listeners
    • Manual override detection
    • Cover service calls
  2. Platform Tests

    • Sensor entities (position, control method, times)
    • Switch entities (automatic control, climate mode)
    • Binary sensors (sun visibility, manual override)
    • Button entities (manual override reset)
  3. Integration Tests

    • Full setup/teardown flows
    • Config entry lifecycle
    • Entity registration
    • Real-time updates
  4. Config Flow Tests

    • Multi-step configuration
    • Validation logic
    • Option forms
    • Error handling

Estimated Additional Tests Needed

  • Coordinator: ~100 tests
  • Platforms: ~80 tests
  • Integration: ~50 tests
  • Config Flow: ~60 tests

Total target: ~500 tests for comprehensive coverage

Contributing

When adding new tests:

  1. Follow existing test organization patterns
  2. Add fixtures to conftest.py if reusable
  3. Use descriptive test names
  4. Include docstrings explaining test purpose
  5. Test edge cases and error conditions
  6. Run full test suite before committing
  7. Update this document if adding new test categories