# Create a virtual environment
python -m venv venv
# Activate virtual environment
source venv/bin/activate # Linux/macOS
.\venv\Scripts\activate # Windows
# Install development dependencies
pip install -r requirements_dev.txt
Create the following directory structure in your project:
area_occupancy/
├── custom_components/
│ └── area_occupancy/
│ ├── __init__.py
│ ├── manifest.json
│ └── ... (other component files)
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ └── ... (test files)
├── requirements.txt
└── requirements_dev.txt
Create pytest.ini
in the root directory:
[pytest]
testpaths = tests
norecursedirs = .git custom_components
asyncio_mode = auto
The pytest-homeassistant-custom-component
package provides a mock Home Assistant environment for testing. Here's how to use it:
# Run all tests
pytest
# Run specific test file
pytest tests/test_sensor.py
# Run with coverage report
pytest --cov=custom_components.area_occupancy
In your conftest.py
, add these fixtures for mock Home Assistant setup:
import pytest
from unittest.mock import patch
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
@pytest.fixture
def hass(loop):
"""Fixture to provide a test instance of Home Assistant."""
hass = HomeAssistant()
loop.run_until_complete(async_setup_component(hass, "homeassistant", {}))
return hass
@pytest.fixture
def mock_hass_config():
"""Fixture to mock Home Assistant configuration."""
return {
"homeassistant": {
"name": "Test Home",
"latitude": 0,
"longitude": 0,
"elevation": 0,
"unit_system": "metric",
"time_zone": "UTC",
}
}
Example of testing with mock entities:
async def test_sensor_behavior(hass):
"""Test sensor behavior with mock entities."""
# Set up mock motion sensor
hass.states.async_set("binary_sensor.motion", "off")
# Configure integration
entry = MockConfigEntry(
domain="area_occupancy",
data={
"name": "Test Area",
"motion_sensors": ["binary_sensor.motion"]
}
)
entry.add_to_hass(hass)
# Initialize integration
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
# Test sensor behavior
hass.states.async_set("binary_sensor.motion", "on")
await hass.async_block_till_done()
state = hass.states.get("sensor.test_area_occupancy_probability")
assert state is not None
assert float(state.state) > 50 # Probability should be high with motion
Example of mocking coordinator updates:
from unittest.mock import patch
async def test_coordinator_update(hass):
"""Test coordinator update with mocked data."""
with patch(
"custom_components.area_occupancy.coordinator.AreaOccupancyCoordinator._async_update_data",
return_value={"probability": 0.75}
):
# Test code here
pass
Example of testing configuration flow:
async def test_config_flow(hass):
"""Test the config flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}
# Test form submission
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"name": "Test Area",
"motion_sensors": ["binary_sensor.motion"]
}
)
assert result2["type"] == "create_entry"
async def test_state_changes(hass):
"""Test response to state changes."""
# Initial setup
await setup_integration(hass)
# Change states
hass.states.async_set("binary_sensor.motion", "on")
await hass.async_block_till_done()
# Assert expected behavior
state = hass.states.get("sensor.area_occupancy_probability")
assert state is not None
from freezegun import freeze_time
async def test_decay(hass):
"""Test time-based decay."""
await setup_integration(hass)
with freeze_time("2024-01-01 12:00:00"):
# Initial state
hass.states.async_set("binary_sensor.motion", "on")
await hass.async_block_till_done()
initial_state = hass.states.get("sensor.area_occupancy_probability")
with freeze_time("2024-01-01 12:05:00"):
# 5 minutes later
await hass.async_block_till_done()
later_state = hass.states.get("sensor.area_occupancy_probability")
assert float(later_state.state) < float(initial_state.state)
async def test_error_handling(hass):
"""Test error handling."""
with patch(
"custom_components.area_occupancy.coordinator.AreaOccupancyCoordinator._async_update_data",
side_effect=Exception("Test error")
):
# Test error handling code
pass
- Use
async_block_till_done()
after state changes - Mock time-dependent functions using
freeze_time
- Test both success and failure paths
- Use coverage reports to identify untested code
- Test edge cases and error conditions
- Mock external dependencies consistently
Add this GitHub Actions workflow for automated testing:
name: Tests
on: [push, pull_request]
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements_dev.txt
- name: Run tests
run: |
pytest --cov=custom_components.area_occupancy
- name: Upload coverage
uses: codecov/codecov-action@v3