Use snapshot testing for APCUPSD integration (#130770)

* First try to use snapshot testing for sensors

* Use snapshot testing

* Add ambr files

* Update comment

* Address review comments

* Remove duplicate async init integration call

* Add device test for cases w/o SERIALNO

* Use friendlier snapshot names

* Use * to mandate keyed argument for async_init_integration

* Always pass mock config entry ID

* Fix incorrect ID
This commit is contained in:
Yuxin Wang 2025-05-14 10:04:07 -04:00 committed by GitHub
parent d273a92a19
commit 11644d48ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 2244 additions and 196 deletions

View File

@ -82,13 +82,18 @@ MOCK_MINIMAL_STATUS: Final = OrderedDict(
async def async_init_integration( async def async_init_integration(
hass: HomeAssistant, host: str = "test", status: dict[str, str] | None = None hass: HomeAssistant,
*,
host: str = "test",
status: dict[str, str] | None = None,
entry_id: str = "mocked-config-entry-id",
) -> MockConfigEntry: ) -> MockConfigEntry:
"""Set up the APC UPS Daemon integration in HomeAssistant.""" """Set up the APC UPS Daemon integration in HomeAssistant."""
if status is None: if status is None:
status = MOCK_STATUS status = MOCK_STATUS
entry = MockConfigEntry( entry = MockConfigEntry(
entry_id=entry_id,
version=1, version=1,
domain=DOMAIN, domain=DOMAIN,
title="APCUPSd", title="APCUPSd",

View File

@ -0,0 +1,48 @@
# serializer version: 1
# name: test_binary_sensor[binary_sensor.myups_online_status-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.myups_online_status',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Online status',
'platform': 'apcupsd',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'online_status',
'unique_id': 'XXXXXXXXXXXX_statflag',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensor[binary_sensor.myups_online_status-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'MyUPS Online status',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.myups_online_status',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---

View File

@ -0,0 +1,133 @@
# serializer version: 1
# name: test_async_setup_entry[status0][device_MyUPS_XXXXXXXXXXXX]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': '928.a8 .D USB FW:a8',
'id': <ANY>,
'identifiers': set({
tuple(
'apcupsd',
'XXXXXXXXXXXX',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'APC',
'model': 'Back-UPS ES 600',
'model_id': None,
'name': 'MyUPS',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': '3.14.14 (31 May 2016) unknown',
'via_device_id': None,
})
# ---
# name: test_async_setup_entry[status1][device_APC UPS_XXXX]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'apcupsd',
'XXXX',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'APC',
'model': None,
'model_id': None,
'name': 'APC UPS',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': None,
'via_device_id': None,
})
# ---
# name: test_async_setup_entry[status2][device_APC UPS_<no serial>]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'apcupsd',
'mocked-config-entry-id',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'APC',
'model': None,
'model_id': None,
'name': 'APC UPS',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': None,
'via_device_id': None,
})
# ---
# name: test_async_setup_entry[status3][device_APC UPS_Blank]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'apcupsd',
'mocked-config-entry-id',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'APC',
'model': None,
'model_id': None,
'name': 'APC UPS',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': None,
'via_device_id': None,
})
# ---

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +1,29 @@
"""Test binary sensors of APCUPSd integration.""" """Test binary sensors of APCUPSd integration."""
import pytest from unittest.mock import patch
import pytest
from syrupy import SnapshotAssertion
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.util import slugify from homeassistant.util import slugify
from . import MOCK_STATUS, async_init_integration from . import MOCK_STATUS, async_init_integration
from tests.common import snapshot_platform
async def test_binary_sensor( async def test_binary_sensor(
hass: HomeAssistant, entity_registry: er.EntityRegistry hass: HomeAssistant,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None: ) -> None:
"""Test states of binary sensor.""" """Test states of binary sensors."""
await async_init_integration(hass, status=MOCK_STATUS) with patch("homeassistant.components.apcupsd.PLATFORMS", [Platform.BINARY_SENSOR]):
config_entry = await async_init_integration(hass, status=MOCK_STATUS)
device_slug, serialno = slugify(MOCK_STATUS["UPSNAME"]), MOCK_STATUS["SERIALNO"] await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
state = hass.states.get(f"binary_sensor.{device_slug}_online_status")
assert state
assert state.state == "on"
entry = entity_registry.async_get(f"binary_sensor.{device_slug}_online_status")
assert entry
assert entry.unique_id == f"{serialno}_statflag"
async def test_no_binary_sensor(hass: HomeAssistant) -> None: async def test_no_binary_sensor(hass: HomeAssistant) -> None:

View File

@ -5,6 +5,7 @@ from collections import OrderedDict
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.apcupsd.const import DOMAIN from homeassistant.components.apcupsd.const import DOMAIN
from homeassistant.components.apcupsd.coordinator import UPDATE_INTERVAL from homeassistant.components.apcupsd.coordinator import UPDATE_INTERVAL
@ -12,6 +13,7 @@ from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity_platform import async_get_platforms
from homeassistant.util import slugify, utcnow from homeassistant.util import slugify, utcnow
from . import CONF_DATA, MOCK_MINIMAL_STATUS, MOCK_STATUS, async_init_integration from . import CONF_DATA, MOCK_MINIMAL_STATUS, MOCK_STATUS, async_init_integration
@ -28,71 +30,31 @@ from tests.common import MockConfigEntry, async_fire_time_changed
# Contains "SERIALNO" but no "UPSNAME" field. # Contains "SERIALNO" but no "UPSNAME" field.
# We should create devices for the entities and prefix their IDs with default "APC UPS". # We should create devices for the entities and prefix their IDs with default "APC UPS".
MOCK_MINIMAL_STATUS | {"SERIALNO": "XXXX"}, MOCK_MINIMAL_STATUS | {"SERIALNO": "XXXX"},
# Does not contain either "SERIALNO" field or "UPSNAME" field. Our integration should work # Does not contain either "SERIALNO" field or "UPSNAME" field.
# fine without it by falling back to config entry ID as unique ID and "APC UPS" as default name. # Our integration should work fine without it by falling back to config entry ID as unique
# ID and "APC UPS" as default name.
MOCK_MINIMAL_STATUS, MOCK_MINIMAL_STATUS,
# Some models report "Blank" as SERIALNO, but we should treat it as not reported. # Some models report "Blank" as SERIALNO, but we should treat it as not reported.
MOCK_MINIMAL_STATUS | {"SERIALNO": "Blank"}, MOCK_MINIMAL_STATUS | {"SERIALNO": "Blank"},
], ],
) )
async def test_async_setup_entry(hass: HomeAssistant, status: OrderedDict) -> None: async def test_async_setup_entry(
"""Test a successful setup entry.""" hass: HomeAssistant,
await async_init_integration(hass, status=status) status: OrderedDict,
device_registry: dr.DeviceRegistry,
prefix = slugify(status.get("UPSNAME", "APC UPS")) + "_" snapshot: SnapshotAssertion,
# Verify successful setup by querying the status sensor.
state = hass.states.get(f"binary_sensor.{prefix}online_status")
assert state
assert state.state != STATE_UNAVAILABLE
assert state.state == "on"
@pytest.mark.parametrize(
"status",
[
# We should not create device entries if SERIALNO is not reported.
MOCK_MINIMAL_STATUS,
# Some models report "Blank" as SERIALNO, but we should treat it as not reported.
MOCK_MINIMAL_STATUS | {"SERIALNO": "Blank"},
# We should set the device name to be the friendly UPSNAME field if available.
MOCK_MINIMAL_STATUS | {"SERIALNO": "XXXX", "UPSNAME": "MyUPS"},
# Otherwise, we should fall back to default device name --- "APC UPS".
MOCK_MINIMAL_STATUS | {"SERIALNO": "XXXX"},
# We should create all fields of the device entry if they are available.
MOCK_STATUS,
],
)
async def test_device_entry(
hass: HomeAssistant, status: OrderedDict, device_registry: dr.DeviceRegistry
) -> None: ) -> None:
"""Test successful setup of device entries.""" """Test a successful setup entry."""
config_entry = await async_init_integration(hass, status=status) config_entry = await async_init_integration(hass, status=status)
device_entry = device_registry.async_get_device(
# Verify device info is properly set up. identifiers={(DOMAIN, config_entry.unique_id or config_entry.entry_id)}
assert len(device_registry.devices) == 1
entry = device_registry.async_get_device(
{(DOMAIN, config_entry.unique_id or config_entry.entry_id)}
) )
assert entry is not None name = f"device_{device_entry.name}_{status.get('SERIALNO', '<no serial>')}"
# Specify the mapping between field name and the expected fields in device entry. assert device_entry == snapshot(name=name)
fields = {
"UPSNAME": entry.name,
"MODEL": entry.model,
"VERSION": entry.sw_version,
"FIRMWARE": entry.hw_version,
}
for field, entry_value in fields.items(): platforms = async_get_platforms(hass, DOMAIN)
if field in status: assert len(platforms) > 0
assert entry_value == status[field] assert all(len(p.entities) > 0 for p in platforms)
# Even if UPSNAME is not available, we must fall back to default "APC UPS".
elif field == "UPSNAME":
assert entry_value == "APC UPS"
else:
assert not entry_value
assert entry.manufacturer == "APC"
async def test_multiple_integrations(hass: HomeAssistant) -> None: async def test_multiple_integrations(hass: HomeAssistant) -> None:
@ -101,8 +63,12 @@ async def test_multiple_integrations(hass: HomeAssistant) -> None:
status1 = MOCK_STATUS | {"LOADPCT": "15.0 Percent", "SERIALNO": "XXXXX1"} status1 = MOCK_STATUS | {"LOADPCT": "15.0 Percent", "SERIALNO": "XXXXX1"}
status2 = MOCK_STATUS | {"LOADPCT": "16.0 Percent", "SERIALNO": "XXXXX2"} status2 = MOCK_STATUS | {"LOADPCT": "16.0 Percent", "SERIALNO": "XXXXX2"}
entries = ( entries = (
await async_init_integration(hass, host="test1", status=status1), await async_init_integration(
await async_init_integration(hass, host="test2", status=status2), hass, host="test1", status=status1, entry_id="entry-id-1"
),
await async_init_integration(
hass, host="test2", status=status2, entry_id="entry-id-2"
),
) )
assert len(hass.config_entries.async_entries(DOMAIN)) == 2 assert len(hass.config_entries.async_entries(DOMAIN)) == 2
@ -121,8 +87,12 @@ async def test_multiple_integrations_different_devices(hass: HomeAssistant) -> N
status1 = MOCK_STATUS | {"SERIALNO": "XXXXX1", "UPSNAME": "MyUPS1"} status1 = MOCK_STATUS | {"SERIALNO": "XXXXX1", "UPSNAME": "MyUPS1"}
status2 = MOCK_STATUS | {"SERIALNO": "XXXXX2", "UPSNAME": "MyUPS2"} status2 = MOCK_STATUS | {"SERIALNO": "XXXXX2", "UPSNAME": "MyUPS2"}
entries = ( entries = (
await async_init_integration(hass, host="test1", status=status1), await async_init_integration(
await async_init_integration(hass, host="test2", status=status2), hass, host="test1", status=status1, entry_id="entry-id-1"
),
await async_init_integration(
hass, host="test2", status=status2, entry_id="entry-id-2"
),
) )
assert len(hass.config_entries.async_entries(DOMAIN)) == 2 assert len(hass.config_entries.async_entries(DOMAIN)) == 2
@ -159,8 +129,12 @@ async def test_unload_remove_entry(hass: HomeAssistant) -> None:
"""Test successful unload and removal of an entry.""" """Test successful unload and removal of an entry."""
# Load two integrations from two mock hosts. # Load two integrations from two mock hosts.
entries = ( entries = (
await async_init_integration(hass, host="test1", status=MOCK_STATUS), await async_init_integration(
await async_init_integration(hass, host="test2", status=MOCK_MINIMAL_STATUS), hass, host="test1", status=MOCK_STATUS, entry_id="entry-id-1"
),
await async_init_integration(
hass, host="test2", status=MOCK_MINIMAL_STATUS, entry_id="entry-id-2"
),
) )
# Assert they are loaded. # Assert they are loaded.

View File

@ -3,22 +3,15 @@
from datetime import timedelta from datetime import timedelta
from unittest.mock import patch from unittest.mock import patch
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.apcupsd.coordinator import REQUEST_REFRESH_COOLDOWN from homeassistant.components.apcupsd.coordinator import REQUEST_REFRESH_COOLDOWN
from homeassistant.components.sensor import (
ATTR_STATE_CLASS,
SensorDeviceClass,
SensorStateClass,
)
from homeassistant.const import ( from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
ATTR_UNIT_OF_MEASUREMENT,
PERCENTAGE,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
STATE_UNKNOWN, STATE_UNKNOWN,
UnitOfElectricPotential, Platform,
UnitOfPower,
UnitOfTime,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
@ -28,118 +21,19 @@ from homeassistant.util.dt import utcnow
from . import MOCK_MINIMAL_STATUS, MOCK_STATUS, async_init_integration from . import MOCK_MINIMAL_STATUS, MOCK_STATUS, async_init_integration
from tests.common import async_fire_time_changed from tests.common import async_fire_time_changed, snapshot_platform
async def test_sensor(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> None: @pytest.mark.usefixtures("entity_registry_enabled_by_default")
"""Test states of sensor.""" async def test_sensor(
await async_init_integration(hass, status=MOCK_STATUS) hass: HomeAssistant,
device_slug, serialno = slugify(MOCK_STATUS["UPSNAME"]), MOCK_STATUS["SERIALNO"] entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
# Test a representative string sensor.
state = hass.states.get(f"sensor.{device_slug}_mode")
assert state
assert state.state == "Stand Alone"
entry = entity_registry.async_get(f"sensor.{device_slug}_mode")
assert entry
assert entry.unique_id == f"{serialno}_upsmode"
# Test two representative voltage sensors.
state = hass.states.get(f"sensor.{device_slug}_input_voltage")
assert state
assert state.state == "124.0"
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfElectricPotential.VOLT
)
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE
entry = entity_registry.async_get(f"sensor.{device_slug}_input_voltage")
assert entry
assert entry.unique_id == f"{serialno}_linev"
state = hass.states.get(f"sensor.{device_slug}_battery_voltage")
assert state
assert state.state == "13.7"
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfElectricPotential.VOLT
)
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE
entry = entity_registry.async_get(f"sensor.{device_slug}_battery_voltage")
assert entry
assert entry.unique_id == f"{serialno}_battv"
# Test a representative time sensor.
state = hass.states.get(f"sensor.{device_slug}_self_test_interval")
assert state
assert state.state == "7"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTime.DAYS
entry = entity_registry.async_get(f"sensor.{device_slug}_self_test_interval")
assert entry
assert entry.unique_id == f"{serialno}_stesti"
# Test a representative percentage sensor.
state = hass.states.get(f"sensor.{device_slug}_load")
assert state
assert state.state == "14.0"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
entry = entity_registry.async_get(f"sensor.{device_slug}_load")
assert entry
assert entry.unique_id == f"{serialno}_loadpct"
# Test a representative wattage sensor.
state = hass.states.get(f"sensor.{device_slug}_nominal_output_power")
assert state
assert state.state == "330"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER
entry = entity_registry.async_get(f"sensor.{device_slug}_nominal_output_power")
assert entry
assert entry.unique_id == f"{serialno}_nompower"
async def test_sensor_name(hass: HomeAssistant) -> None:
"""Test if sensor name follows the recommended entity naming scheme.
See https://developers.home-assistant.io/docs/core/entity/#entity-naming for more details.
"""
await async_init_integration(hass, status=MOCK_STATUS)
all_states = hass.states.async_all()
assert len(all_states) != 0
device_name = MOCK_STATUS["UPSNAME"]
for state in all_states:
# Friendly name must start with the device name.
friendly_name = state.name
assert friendly_name.startswith(device_name)
# Entity names should start with a capital letter, the rest of the words are lower case.
entity_name = friendly_name.removeprefix(device_name).strip()
assert entity_name == entity_name.capitalize()
async def test_sensor_disabled(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None: ) -> None:
"""Test sensor disabled by default.""" """Test states of sensor."""
await async_init_integration(hass) with patch("homeassistant.components.apcupsd.PLATFORMS", [Platform.SENSOR]):
config_entry = await async_init_integration(hass, status=MOCK_STATUS)
device_slug, serialno = slugify(MOCK_STATUS["UPSNAME"]), MOCK_STATUS["SERIALNO"] await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
# Test a representative integration-disabled sensor.
entry = entity_registry.async_get(f"sensor.{device_slug}_model")
assert entry.disabled
assert entry.unique_id == f"{serialno}_model"
assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
# Test enabling entity.
updated_entry = entity_registry.async_update_entity(
entry.entity_id, disabled_by=None
)
assert updated_entry != entry
assert updated_entry.disabled is False
async def test_state_update(hass: HomeAssistant) -> None: async def test_state_update(hass: HomeAssistant) -> None:
@ -241,7 +135,7 @@ async def test_multiple_manual_update_entity(hass: HomeAssistant) -> None:
async def test_sensor_unknown(hass: HomeAssistant) -> None: async def test_sensor_unknown(hass: HomeAssistant) -> None:
"""Test if our integration can properly certain sensors as unknown when it becomes so.""" """Test if our integration can properly mark certain sensors as unknown when it becomes so."""
await async_init_integration(hass, status=MOCK_MINIMAL_STATUS) await async_init_integration(hass, status=MOCK_MINIMAL_STATUS)
ups_mode_id = "sensor.apc_ups_mode" ups_mode_id = "sensor.apc_ups_mode"