From 0bf2f6a94c3d4aa9b679d89a5913bd32e5fbda44 Mon Sep 17 00:00:00 2001 From: Whitney Young Date: Thu, 11 Jul 2024 14:48:07 -0700 Subject: [PATCH] Use Syrupy for assertions Also, refactor to use fixtures to setup tests. --- requirements.test.txt | 1 + tests/conftest.py | 86 ++++++++++++- tests/snapshots/test_sensor.ambr | 203 +++++++++++++++++++++++++++++++ tests/test_sensor.py | 197 ++++-------------------------- 4 files changed, 311 insertions(+), 176 deletions(-) create mode 100644 tests/snapshots/test_sensor.ambr diff --git a/requirements.test.txt b/requirements.test.txt index 9d84371..f56eff0 100644 --- a/requirements.test.txt +++ b/requirements.test.txt @@ -3,3 +3,4 @@ pre-commit==3.7.1 pytest==8.2.0 pytest-cov==5.0.0 pytest-homeassistant-custom-component==0.13.135 +syrupy==4.6.1 diff --git a/tests/conftest.py b/tests/conftest.py index 06d7403..6208a2d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,15 +1,32 @@ """Fixtures for testing.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, patch, PropertyMock from pathlib import Path +from homeassistant.core import HomeAssistant + import json import pytest from typing import Generator +from pytest_homeassistant_custom_component.common import ( + MockConfigEntry, +) + +from custom_components.watersmart.client import AuthenticationError +from custom_components.watersmart.const import DOMAIN + FIXTURES_DIR = Path(__file__).parent.joinpath("fixtures") +class AdvacnedPropertyMock(PropertyMock): + def __get__(self, obj, obj_type=None): + return self(obj) + + def __set__(self, obj, val): + self(obj, val) + + class FixtureLoader: """Fixture loader.""" @@ -78,10 +95,10 @@ def mock_aiohttp_session() -> Generator[dict[str, AsyncMock], None, None]: @pytest.fixture -def mock_watersmart_client() -> Generator[AsyncMock, None, None]: +def mock_watersmart_client(fixture_loader) -> Generator[AsyncMock, None, None]: """Mock a WaterSmart client.""" - hourly_data = FixtureLoader().realtime_api_response_obj["data"]["series"] + hourly_data = fixture_loader.realtime_api_response_obj["data"]["series"] with ( patch( @@ -105,3 +122,66 @@ def mock_watersmart_client() -> Generator[AsyncMock, None, None]: client.async_get_hourly_data.return_value = hourly_data yield client + + +@pytest.fixture +def client_authentication_error(mock_watersmart_client): + mock_watersmart_client.async_get_hourly_data.side_effect = AuthenticationError( + "invalid credentials" + ) + + +@pytest.fixture +def mock_sensor_name() -> Generator[PropertyMock, None, None]: + """Mock sensor names. + + This testing setup/library does not use `strings.json` and the entity description to translation key + to get the entity name, so it's being patched here to just use the translaiton key. That way we at least + get entity ids that are closer to what they will really be. + """ + + with patch( + "homeassistant.components.sensor.SensorEntity.name", + new_callable=AdvacnedPropertyMock, + ) as mock_name: + + def name_from_entity_description(sensor): + return sensor.entity_description.translation_key.replace( + "_", " " + ).capitalize() + + mock_name.side_effect = lambda self: ( + name_from_entity_description(self) if self else None + ) + + yield mock_name + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + "host": "test", + "username": "test@home-assistant.io", + "password": "Passw0rd", + }, + ) + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_sensor_name: Generator[PropertyMock, None, None], + mock_watersmart_client: Generator[AsyncMock, None, None], +) -> MockConfigEntry: + """Set up the WaterSmart integration for testing.""" + + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/snapshots/test_sensor.ambr b/tests/snapshots/test_sensor.ambr new file mode 100644 index 0000000..48de589 --- /dev/null +++ b/tests/snapshots/test_sensor.ambr @@ -0,0 +1,203 @@ +# serializer version: 1 +# name: test_most_recent_day_sensor + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data scraped from WaterSmart', + 'device_class': 'water', + 'friendly_name': 'WaterSmart (test) Gallons for most recent full day', + 'related': list([ + dict({ + 'gallons': 1, + 'start': '2024-06-20T00:00:00-07:00', + }), + dict({ + 'gallons': 2, + 'start': '2024-06-20T01:00:00-07:00', + }), + dict({ + 'gallons': 3, + 'start': '2024-06-20T02:00:00-07:00', + }), + dict({ + 'gallons': 4, + 'start': '2024-06-20T03:00:00-07:00', + }), + dict({ + 'gallons': 5, + 'start': '2024-06-20T04:00:00-07:00', + }), + dict({ + 'gallons': 6, + 'start': '2024-06-20T05:00:00-07:00', + }), + dict({ + 'gallons': 7, + 'start': '2024-06-20T06:00:00-07:00', + }), + dict({ + 'gallons': 8, + 'start': '2024-06-20T07:00:00-07:00', + }), + dict({ + 'gallons': 9, + 'start': '2024-06-20T08:00:00-07:00', + }), + dict({ + 'gallons': 10, + 'start': '2024-06-20T09:00:00-07:00', + }), + dict({ + 'gallons': 11, + 'start': '2024-06-20T10:00:00-07:00', + }), + dict({ + 'gallons': 12, + 'start': '2024-06-20T11:00:00-07:00', + }), + dict({ + 'gallons': 13, + 'start': '2024-06-20T12:00:00-07:00', + }), + dict({ + 'gallons': 14, + 'start': '2024-06-20T13:00:00-07:00', + }), + dict({ + 'gallons': 15, + 'start': '2024-06-20T14:00:00-07:00', + }), + dict({ + 'gallons': 16, + 'start': '2024-06-20T15:00:00-07:00', + }), + dict({ + 'gallons': 17, + 'start': '2024-06-20T16:00:00-07:00', + }), + dict({ + 'gallons': 18, + 'start': '2024-06-20T17:00:00-07:00', + }), + dict({ + 'gallons': 19, + 'start': '2024-06-20T18:00:00-07:00', + }), + dict({ + 'gallons': 20, + 'start': '2024-06-20T19:00:00-07:00', + }), + dict({ + 'gallons': 21, + 'start': '2024-06-20T20:00:00-07:00', + }), + dict({ + 'gallons': 22, + 'start': '2024-06-20T21:00:00-07:00', + }), + dict({ + 'gallons': 23, + 'start': '2024-06-20T22:00:00-07:00', + }), + dict({ + 'gallons': 24, + 'start': '2024-06-20T23:00:00-07:00', + }), + ]), + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.watersmart_test_gallons_for_most_recent_full_day', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1136', + }) +# --- +# name: test_most_recent_hour_sensor + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data scraped from WaterSmart', + 'device_class': 'water', + 'friendly_name': 'WaterSmart (test) Gallons for most recent hour', + 'related': list([ + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 14.3, + 'start': '2024-06-19T22:00:00-07:00', + }), + ]), + 'start': '2024-06-19T22:00:00-07:00', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.watersmart_test_gallons_for_most_recent_hour', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '54.1', + }) +# --- +# name: test_sensors_for_zero_gallons + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data scraped from WaterSmart', + 'device_class': 'water', + 'friendly_name': 'WaterSmart (test) Gallons for most recent full day', + 'related': list([ + ]), + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.watersmart_test_gallons_for_most_recent_full_day', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors_for_zero_gallons.1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data scraped from WaterSmart', + 'device_class': 'water', + 'friendly_name': 'WaterSmart (test) Gallons for most recent hour', + 'related': list([ + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T22:00:00-07:00', + }), + ]), + 'start': '2024-06-19T22:00:00-07:00', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.watersmart_test_gallons_for_most_recent_hour', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- diff --git a/tests/test_sensor.py b/tests/test_sensor.py index 3b9dd88..5c601f8 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -1,60 +1,18 @@ """Test sensor for simple integration.""" -from homeassistant.const import UnitOfVolume from homeassistant.core import HomeAssistant from homeassistant.util.dt import utcnow import datetime as dt import pytest -from unittest.mock import patch, PropertyMock from pytest_homeassistant_custom_component.common import ( - MockConfigEntry, async_fire_time_changed, ) -from typing import Generator - -from custom_components.watersmart.client import AuthenticationError -from custom_components.watersmart.const import DOMAIN - - -class AdvacnedPropertyMock(PropertyMock): - def __get__(self, obj, obj_type=None): - return self(obj) - - def __set__(self, obj, val): - self(obj, val) +from syrupy.assertion import SnapshotAssertion @pytest.fixture -def mock_sensor_name() -> Generator[PropertyMock, None, None]: - """Mock sensor names. - - This testing setup/library does not use `strings.json` and the entity description to translation key - to get the entity name, so it's being patched here to just use the translaiton key. That way we at least - get entity ids that are closer to what they will really be. - """ - - with patch( - "homeassistant.components.sensor.SensorEntity.name", - new_callable=AdvacnedPropertyMock, - ) as mock_name: - - def name_from_entity_description(sensor): - return sensor.entity_description.translation_key.replace( - "_", " " - ).capitalize() - - mock_name.side_effect = lambda self: ( - name_from_entity_description(self) if self else None - ) - - yield mock_name - - -async def test_most_recent_day_sensor( - hass: HomeAssistant, mock_watersmart_client, mock_sensor_name -): - """Test sensor.""" +def client_hourly_data_full_day(mock_watersmart_client): hourly = mock_watersmart_client.async_get_hourly_data.return_value for gallons in range(25): @@ -72,151 +30,61 @@ async def test_most_recent_day_sensor( mock_watersmart_client.async_get_hourly_data.return_value = hourly - entry = MockConfigEntry( - domain=DOMAIN, - data={ - "host": "test", - "username": "test@home-assistant.io", - "password": "Passw0rd", - }, - ) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() +@pytest.mark.usefixtures("client_hourly_data_full_day", "init_integration") +async def test_most_recent_day_sensor( + hass: HomeAssistant, mock_watersmart_client, snapshot: SnapshotAssertion +): + """Test sensor.""" recent_day_sensor_state = hass.states.get( "sensor.watersmart_test_gallons_for_most_recent_full_day" ) - assert recent_day_sensor_state - assert recent_day_sensor_state.state == "1136" # gallons changed to liters - assert recent_day_sensor_state.attributes == { - "attribution": "Data scraped from WaterSmart", - "friendly_name": "WaterSmart (test) Gallons for most recent full day", - "device_class": "water", - "unit_of_measurement": UnitOfVolume.LITERS, - "related": [ - {"start": "2024-06-20T00:00:00-07:00", "gallons": 1}, - {"start": "2024-06-20T01:00:00-07:00", "gallons": 2}, - {"start": "2024-06-20T02:00:00-07:00", "gallons": 3}, - {"start": "2024-06-20T03:00:00-07:00", "gallons": 4}, - {"start": "2024-06-20T04:00:00-07:00", "gallons": 5}, - {"start": "2024-06-20T05:00:00-07:00", "gallons": 6}, - {"start": "2024-06-20T06:00:00-07:00", "gallons": 7}, - {"start": "2024-06-20T07:00:00-07:00", "gallons": 8}, - {"start": "2024-06-20T08:00:00-07:00", "gallons": 9}, - {"start": "2024-06-20T09:00:00-07:00", "gallons": 10}, - {"start": "2024-06-20T10:00:00-07:00", "gallons": 11}, - {"start": "2024-06-20T11:00:00-07:00", "gallons": 12}, - {"start": "2024-06-20T12:00:00-07:00", "gallons": 13}, - {"start": "2024-06-20T13:00:00-07:00", "gallons": 14}, - {"start": "2024-06-20T14:00:00-07:00", "gallons": 15}, - {"start": "2024-06-20T15:00:00-07:00", "gallons": 16}, - {"start": "2024-06-20T16:00:00-07:00", "gallons": 17}, - {"start": "2024-06-20T17:00:00-07:00", "gallons": 18}, - {"start": "2024-06-20T18:00:00-07:00", "gallons": 19}, - {"start": "2024-06-20T19:00:00-07:00", "gallons": 20}, - {"start": "2024-06-20T20:00:00-07:00", "gallons": 21}, - {"start": "2024-06-20T21:00:00-07:00", "gallons": 22}, - {"start": "2024-06-20T22:00:00-07:00", "gallons": 23}, - {"start": "2024-06-20T23:00:00-07:00", "gallons": 24}, - ], - } - + assert snapshot == recent_day_sensor_state assert mock_watersmart_client.async_get_hourly_data.call_count == 1 -async def test_most_recent_hour_sensor( - hass: HomeAssistant, mock_watersmart_client, mock_sensor_name -): - """Test sensor.""" +@pytest.fixture +def client_hourly_data_recent_hour_higher(mock_watersmart_client): mock_watersmart_client.async_get_hourly_data.return_value[-1]["gallons"] = 14.3 - entry = MockConfigEntry( - domain=DOMAIN, - data={ - "host": "test", - "username": "test@home-assistant.io", - "password": "Passw0rd", - }, - ) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() +@pytest.mark.usefixtures("client_hourly_data_recent_hour_higher", "init_integration") +async def test_most_recent_hour_sensor( + hass: HomeAssistant, mock_watersmart_client, snapshot: SnapshotAssertion +): + """Test sensor.""" recent_hour_sensor_state = hass.states.get( "sensor.watersmart_test_gallons_for_most_recent_hour" ) - assert recent_hour_sensor_state - assert recent_hour_sensor_state.state == "54.1" # gallons changed to liters - assert recent_hour_sensor_state.attributes == { - "attribution": "Data scraped from WaterSmart", - "friendly_name": "WaterSmart (test) Gallons for most recent hour", - "device_class": "water", - "unit_of_measurement": UnitOfVolume.LITERS, - "start": "2024-06-19T22:00:00-07:00", - "related": [ - {"gallons": 7.48, "start": "2024-06-19T19:00:00-07:00"}, - {"gallons": 0, "start": "2024-06-19T20:00:00-07:00"}, - {"gallons": 7.48, "start": "2024-06-19T21:00:00-07:00"}, - {"gallons": 14.3, "start": "2024-06-19T22:00:00-07:00"}, - ], - } - + assert snapshot == recent_hour_sensor_state assert mock_watersmart_client.async_get_hourly_data.call_count == 1 +@pytest.mark.usefixtures("init_integration") async def test_sensors_for_zero_gallons( - hass: HomeAssistant, mock_watersmart_client, mock_sensor_name + hass: HomeAssistant, mock_watersmart_client, snapshot: SnapshotAssertion ): """Test sensor.""" - - entry = MockConfigEntry( - domain=DOMAIN, - data={ - "host": "test", - "username": "test@home-assistant.io", - "password": "Passw0rd", - }, - ) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - recent_day_sensor_state = hass.states.get( "sensor.watersmart_test_gallons_for_most_recent_full_day" ) - assert recent_day_sensor_state - assert recent_day_sensor_state.state == "0" + assert snapshot == recent_day_sensor_state recent_hour_sensor_state = hass.states.get( "sensor.watersmart_test_gallons_for_most_recent_hour" ) - assert recent_hour_sensor_state - assert recent_hour_sensor_state.state == "0" + assert snapshot == recent_hour_sensor_state assert mock_watersmart_client.async_get_hourly_data.call_count == 1 -async def test_sensor_update( - hass: HomeAssistant, mock_watersmart_client, mock_sensor_name -): +@pytest.mark.usefixtures("init_integration") +async def test_sensor_update(hass: HomeAssistant, mock_watersmart_client): """Test sensor.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={ - "host": "test", - "username": "test@home-assistant.io", - "password": "Passw0rd", - }, - ) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - assert mock_watersmart_client.async_get_hourly_data.call_count == 1 async_fire_time_changed(hass, utcnow() + dt.timedelta(minutes=10)) @@ -230,26 +98,9 @@ async def test_sensor_update( assert mock_watersmart_client.async_get_hourly_data.call_count == 2 -async def test_sensor_update_failure( - hass: HomeAssistant, mock_watersmart_client, mock_sensor_name -): +@pytest.mark.usefixtures("client_authentication_error", "init_integration") +async def test_sensor_update_failure(hass: HomeAssistant, mock_watersmart_client): """Test sensor.""" - mock_watersmart_client.async_get_hourly_data.side_effect = AuthenticationError( - "invalid credentials" - ) - - entry = MockConfigEntry( - domain=DOMAIN, - data={ - "host": "test", - "username": "test@home-assistant.io", - "password": "Passw0rd", - }, - ) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - recent_day_sensor_state = hass.states.get( "sensor.watersmart_test_gallons_for_most_recent_full_day" )