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(
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:
"""Set up the APC UPS Daemon integration in HomeAssistant."""
if status is None:
status = MOCK_STATUS
entry = MockConfigEntry(
entry_id=entry_id,
version=1,
domain=DOMAIN,
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."""
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.helpers import entity_registry as er
from homeassistant.util import slugify
from . import MOCK_STATUS, async_init_integration
from tests.common import snapshot_platform
async def test_binary_sensor(
hass: HomeAssistant, entity_registry: er.EntityRegistry
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test states of binary sensor."""
await async_init_integration(hass, status=MOCK_STATUS)
device_slug, serialno = slugify(MOCK_STATUS["UPSNAME"]), MOCK_STATUS["SERIALNO"]
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"
"""Test states of binary sensors."""
with patch("homeassistant.components.apcupsd.PLATFORMS", [Platform.BINARY_SENSOR]):
config_entry = await async_init_integration(hass, status=MOCK_STATUS)
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
async def test_no_binary_sensor(hass: HomeAssistant) -> None:

View File

@ -5,6 +5,7 @@ from collections import OrderedDict
from unittest.mock import patch
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.apcupsd.const import DOMAIN
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.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity_platform import async_get_platforms
from homeassistant.util import slugify, utcnow
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.
# We should create devices for the entities and prefix their IDs with default "APC UPS".
MOCK_MINIMAL_STATUS | {"SERIALNO": "XXXX"},
# Does not contain either "SERIALNO" field or "UPSNAME" field. Our integration should work
# fine without it by falling back to config entry ID as unique ID and "APC UPS" as default name.
# Does not contain either "SERIALNO" field or "UPSNAME" field.
# 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,
# Some models report "Blank" as SERIALNO, but we should treat it as not reported.
MOCK_MINIMAL_STATUS | {"SERIALNO": "Blank"},
],
)
async def test_async_setup_entry(hass: HomeAssistant, status: OrderedDict) -> None:
"""Test a successful setup entry."""
await async_init_integration(hass, status=status)
prefix = slugify(status.get("UPSNAME", "APC UPS")) + "_"
# 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
async def test_async_setup_entry(
hass: HomeAssistant,
status: OrderedDict,
device_registry: dr.DeviceRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test successful setup of device entries."""
"""Test a successful setup entry."""
config_entry = await async_init_integration(hass, status=status)
# Verify device info is properly set up.
assert len(device_registry.devices) == 1
entry = device_registry.async_get_device(
{(DOMAIN, config_entry.unique_id or config_entry.entry_id)}
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, config_entry.unique_id or config_entry.entry_id)}
)
assert entry is not None
# Specify the mapping between field name and the expected fields in device entry.
fields = {
"UPSNAME": entry.name,
"MODEL": entry.model,
"VERSION": entry.sw_version,
"FIRMWARE": entry.hw_version,
}
name = f"device_{device_entry.name}_{status.get('SERIALNO', '<no serial>')}"
assert device_entry == snapshot(name=name)
for field, entry_value in fields.items():
if field in status:
assert entry_value == status[field]
# 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"
platforms = async_get_platforms(hass, DOMAIN)
assert len(platforms) > 0
assert all(len(p.entities) > 0 for p in platforms)
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"}
status2 = MOCK_STATUS | {"LOADPCT": "16.0 Percent", "SERIALNO": "XXXXX2"}
entries = (
await async_init_integration(hass, host="test1", status=status1),
await async_init_integration(hass, host="test2", status=status2),
await async_init_integration(
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
@ -121,8 +87,12 @@ async def test_multiple_integrations_different_devices(hass: HomeAssistant) -> N
status1 = MOCK_STATUS | {"SERIALNO": "XXXXX1", "UPSNAME": "MyUPS1"}
status2 = MOCK_STATUS | {"SERIALNO": "XXXXX2", "UPSNAME": "MyUPS2"}
entries = (
await async_init_integration(hass, host="test1", status=status1),
await async_init_integration(hass, host="test2", status=status2),
await async_init_integration(
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
@ -159,8 +129,12 @@ async def test_unload_remove_entry(hass: HomeAssistant) -> None:
"""Test successful unload and removal of an entry."""
# Load two integrations from two mock hosts.
entries = (
await async_init_integration(hass, host="test1", status=MOCK_STATUS),
await async_init_integration(hass, host="test2", status=MOCK_MINIMAL_STATUS),
await async_init_integration(
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.

View File

@ -3,22 +3,15 @@
from datetime import timedelta
from unittest.mock import patch
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.apcupsd.coordinator import REQUEST_REFRESH_COOLDOWN
from homeassistant.components.sensor import (
ATTR_STATE_CLASS,
SensorDeviceClass,
SensorStateClass,
)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
ATTR_UNIT_OF_MEASUREMENT,
PERCENTAGE,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
UnitOfElectricPotential,
UnitOfPower,
UnitOfTime,
Platform,
)
from homeassistant.core import HomeAssistant
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 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:
"""Test states of sensor."""
await async_init_integration(hass, status=MOCK_STATUS)
device_slug, serialno = slugify(MOCK_STATUS["UPSNAME"]), MOCK_STATUS["SERIALNO"]
# 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
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_sensor(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test sensor disabled by default."""
await async_init_integration(hass)
device_slug, serialno = slugify(MOCK_STATUS["UPSNAME"]), MOCK_STATUS["SERIALNO"]
# 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
"""Test states of sensor."""
with patch("homeassistant.components.apcupsd.PLATFORMS", [Platform.SENSOR]):
config_entry = await async_init_integration(hass, status=MOCK_STATUS)
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
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:
"""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)
ups_mode_id = "sensor.apc_ups_mode"