This document describes the unit test structure, organization, and coverage for the Adaptive Cover Pro integration.
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)
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)
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
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 aheadtest_gamma_sun_to_left- Positive gamma for sun to the lefttest_gamma_sun_to_right- Negative gamma for sun to the righttest_gamma_wrapping_around_180- Gamma wrapping at ±180° boundariestest_gamma_wrapping_negative- Gamma wrapping in negative direction
Elevation Validation:
test_valid_elevation_with_both_limits- Elevation within min/max rangetest_valid_elevation_below_minimum- Elevation below min thresholdtest_valid_elevation_above_maximum- Elevation above max thresholdtest_valid_elevation_only_min_set- Only minimum elevation configuredtest_valid_elevation_only_max_set- Only maximum elevation configuredtest_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 horizontest_valid_sun_outside_left_fov- Sun invalid outside left FOV boundarytest_valid_sun_outside_right_fov- Sun invalid outside right FOV boundarytest_valid_sun_below_horizon- Sun invalid when below horizontest_valid_sun_at_left_boundary- Boundary condition at left edgetest_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 elevationtest_is_sun_in_blind_spot_elevation_too_high- Elevation above blind spot thresholdtest_is_sun_in_blind_spot_outside_area- Sun outside blind spot azimuth rangetest_is_sun_in_blind_spot_disabled- Blind spot detection disabledtest_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 spottest_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 viewtest_direct_sun_valid_before_sunrise- Returns False before sunrise timetest_direct_sun_valid_with_sunset_offset- Respects sunset offset configuration
Default Position Logic:
test_default_position_before_sunset- Returns h_def before sunsettest_default_position_after_sunset- Returns sunset_pos after sunsettest_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 heighttest_calculate_position_low_sun- Low sun creates shorter blind heighttest_calculate_position_with_gamma_angle- Angled sun increases path lengthtest_calculate_position_clips_to_window_height- Maximum clippingtest_calculate_percentage_standard- Height to percentage conversiontest_calculate_percentage_with_different_window_height- Window height variationstest_calculate_percentage_with_different_distance- Distance variations
AdaptiveHorizontalCover (7 tests):
test_calculate_position_standard- Standard awning extensiontest_calculate_position_with_awning_angle- Non-zero awning angletest_calculate_position_high_sun- High sun minimal shadowtest_calculate_position_low_sun- Low sun longer shadowtest_calculate_percentage_standard- Awning percentage conversiontest_calculate_percentage_with_different_awning_length- Length variationstest_awning_angle_variations- Multiple angle scenarios (0°, 15°, 30°, 45°)
AdaptiveTiltCover (9 tests):
test_beta_property- Beta angle calculation in radianstest_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 mode1test_calculate_percentage_mode2- Percentage conversion mode2test_slat_depth_variations- Various slat depthstest_slat_distance_variations- Various slat distancestest_beta_with_different_sun_angles- Beta at different elevationstest_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 validtest_get_state_sun_invalid- Uses default position when sun invalidtest_get_state_after_sunset- Uses sunset_pos after sunsettest_max_position_clamping- Max position limitingtest_min_position_clamping- Min position limitingtest_min_position_with_bool_flag_sun_valid- Min with direct_sun_valid flagtest_max_position_with_bool_flag_sun_valid- Max with direct_sun_valid flagtest_clipping_to_100- Position clips to valid rangetest_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 sensortest_outside_temperature_from_weather_entity- Reading from weather entitytest_inside_temperature_from_sensor- Reading from temperature sensortest_inside_temperature_from_climate_entity- Reading from climate entitytest_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 awaytest_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 thresholdtest_is_winter_false- Temperature above temp_low thresholdtest_is_summer_true- Temperature above temp_high and outside_hightest_is_summer_false- Temperature below thresholdstest_outside_high_true- Outside temp above threshold
Weather & Light (5 tests):
test_is_sunny_true- Weather state in sunny conditions listtest_is_sunny_false- Weather state not in listtest_lux_below_threshold- Lux value below thresholdtest_lux_above_threshold- Lux value above thresholdtest_lux_disabled- Lux checking disabled (_use_lux=False)test_irradiance_below_threshold- Irradiance below thresholdtest_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_presencetest_normal_type_cover_without_presence- Delegates to normal_without_presencetest_normal_with_presence_winter_sun_valid- Winter: open fully (100%)test_normal_with_presence_not_sunny- Not sunny: use defaulttest_normal_with_presence_summer_transparent- Summer + transparent: 0%test_normal_with_presence_intermediate- Intermediate: calculated positiontest_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 blindstest_get_state_tilt_type- Routes to tilt_state for tilt coverstest_get_state_max_position_clamping- Max position applied in climate statetest_get_state_min_position_clamping- Min position applied in climate state
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 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
# 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# 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# Generate HTML coverage report
pytest tests/ --cov=custom_components/adaptive_cover_pro --cov-report=html
# View report
open htmlcov/index.html# Activate venv
source venv/bin/activate
# Run tests
pytest tests/ -vFixtures are defined in tests/conftest.py and provide reusable test components.
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"})
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
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
@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@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@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@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()@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| 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 |
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
-
Use descriptive test names
- Format:
test_<what>_<condition>_<expected> - Example:
test_gamma_sun_to_leftclearly describes scenario
- Format:
-
One assertion per test when possible
- Makes failures easier to diagnose
- Exception: Related assertions (e.g., type and range)
-
Test edge cases explicitly
- Boundary values (0, 100, min, max)
- None/invalid inputs
- Wrapping/overflow scenarios
-
Mock external dependencies
- Always mock datetime for time-dependent tests
- Mock entity states via fixtures
- Don't make real I/O calls
-
Use fixtures for common setup
- Add to conftest.py if used by multiple tests
- Keep fixtures focused and simple
-
Document complex test scenarios
- Explain non-obvious setup
- Document expected behavior
- Include calculation examples in comments
- 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
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
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))# 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-
Coordinator Tests
- State management
- Entity listeners
- Manual override detection
- Cover service calls
-
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)
-
Integration Tests
- Full setup/teardown flows
- Config entry lifecycle
- Entity registration
- Real-time updates
-
Config Flow Tests
- Multi-step configuration
- Validation logic
- Option forms
- Error handling
- Coordinator: ~100 tests
- Platforms: ~80 tests
- Integration: ~50 tests
- Config Flow: ~60 tests
Total target: ~500 tests for comprehensive coverage
When adding new tests:
- Follow existing test organization patterns
- Add fixtures to conftest.py if reusable
- Use descriptive test names
- Include docstrings explaining test purpose
- Test edge cases and error conditions
- Run full test suite before committing
- Update this document if adding new test categories