From dab66a58ce56e44b6c036295eacdb115941727f2 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 15 May 2021 20:22:32 +0200 Subject: [PATCH] Clean smhi tests (#50681) --- tests/components/smhi/__init__.py | 2 + tests/components/smhi/conftest.py | 10 + tests/components/smhi/test_init.py | 56 +++-- tests/components/smhi/test_weather.py | 316 +++++++++++++------------- 4 files changed, 210 insertions(+), 174 deletions(-) create mode 100644 tests/components/smhi/conftest.py diff --git a/tests/components/smhi/__init__.py b/tests/components/smhi/__init__.py index 100b1f1bbb1..d815aafc8f5 100644 --- a/tests/components/smhi/__init__.py +++ b/tests/components/smhi/__init__.py @@ -1 +1,3 @@ """Tests for the SMHI component.""" +ENTITY_ID = "weather.smhi_test" +TEST_CONFIG = {"name": "test", "longitude": "17.84197", "latitude": "59.32624"} diff --git a/tests/components/smhi/conftest.py b/tests/components/smhi/conftest.py new file mode 100644 index 00000000000..6ededa6d975 --- /dev/null +++ b/tests/components/smhi/conftest.py @@ -0,0 +1,10 @@ +"""Provide common smhi fixtures.""" +import pytest + +from tests.common import load_fixture + + +@pytest.fixture(scope="session") +def api_response(): + """Return an API response.""" + return load_fixture("smhi.json") diff --git a/tests/components/smhi/test_init.py b/tests/components/smhi/test_init.py index ac4177dca7d..ab937d266a4 100644 --- a/tests/components/smhi/test_init.py +++ b/tests/components/smhi/test_init.py @@ -1,30 +1,44 @@ """Test SMHI component setup process.""" -from unittest.mock import Mock +from smhi.smhi_lib import APIURL_TEMPLATE -from homeassistant.components import smhi +from homeassistant.components.smhi.const import DOMAIN +from homeassistant.core import HomeAssistant -from .common import AsyncMock +from . import ENTITY_ID, TEST_CONFIG -TEST_CONFIG = { - "config": { - "name": "0123456789ABCDEF", - "longitude": "62.0022", - "latitude": "17.0022", - } -} +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker -async def test_forward_async_setup_entry() -> None: - """Test that it will forward setup entry.""" - hass = Mock() +async def test_setup_entry( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str +) -> None: + """Test setup entry.""" + uri = APIURL_TEMPLATE.format(TEST_CONFIG["longitude"], TEST_CONFIG["latitude"]) + aioclient_mock.get(uri, text=api_response) + entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) + entry.add_to_hass(hass) - assert await smhi.async_setup_entry(hass, {}) is True - assert len(hass.config_entries.async_setup_platforms.mock_calls) == 1 + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state -async def test_forward_async_unload_entry() -> None: - """Test that it will forward unload entry.""" - hass = AsyncMock() - hass.config_entries.async_unload_platforms = AsyncMock(return_value=True) - assert await smhi.async_unload_entry(hass, {}) is True - assert len(hass.config_entries.async_unload_platforms.mock_calls) == 1 +async def test_remove_entry(hass: HomeAssistant) -> None: + """Test remove entry.""" + entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state + + await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert not state diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index 10a21f74099..33214be0ae3 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -1,18 +1,19 @@ """Test for the smhi weather entity.""" import asyncio -from datetime import datetime -import logging -from unittest.mock import AsyncMock, Mock, patch +from datetime import datetime, timedelta +from unittest.mock import patch -from smhi.smhi_lib import APIURL_TEMPLATE, SmhiForecastException +import pytest +from smhi.smhi_lib import APIURL_TEMPLATE, SmhiForecast, SmhiForecastException -from homeassistant.components.smhi import weather as weather_smhi from homeassistant.components.smhi.const import ( ATTR_SMHI_CLOUDINESS, ATTR_SMHI_THUNDER_PROBABILITY, ATTR_SMHI_WIND_GUST_SPEED, ) +from homeassistant.components.smhi.weather import CONDITION_CLASSES, RETRY_TIMEOUT from homeassistant.components.weather import ( + ATTR_FORECAST, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, @@ -25,40 +26,36 @@ from homeassistant.components.weather import ( ATTR_WEATHER_VISIBILITY, ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, - DOMAIN as WEATHER_DOMAIN, ) -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow -from tests.common import MockConfigEntry, load_fixture +from . import ENTITY_ID, TEST_CONFIG -_LOGGER = logging.getLogger(__name__) - -TEST_CONFIG = {"name": "test", "longitude": "17.84197", "latitude": "59.32624"} +from tests.common import MockConfigEntry, async_fire_time_changed +from tests.test_util.aiohttp import AiohttpClientMocker -async def test_setup_hass(hass: HomeAssistant, aioclient_mock) -> None: - """Test for successfully setting up the smhi platform. - - This test are deeper integrated with the core. Since only - config_flow is used the component are setup with - "async_forward_entry_setup". The actual result are tested - with the entity state rather than "per function" unity tests - """ +async def test_setup_hass( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str +) -> None: + """Test for successfully setting up the smhi integration.""" uri = APIURL_TEMPLATE.format(TEST_CONFIG["longitude"], TEST_CONFIG["latitude"]) - api_response = load_fixture("smhi.json") aioclient_mock.get(uri, text=api_response) entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) + entry.add_to_hass(hass) - await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert aioclient_mock.call_count == 1 # Testing the actual entity state for # deeper testing than normal unity test - state = hass.states.get("weather.smhi_test") + state = hass.states.get(ENTITY_ID) + assert state assert state.state == "sunny" assert state.attributes[ATTR_SMHI_CLOUDINESS] == 50 assert state.attributes[ATTR_SMHI_THUNDER_PROBABILITY] == 33 @@ -70,7 +67,6 @@ async def test_setup_hass(hass: HomeAssistant, aioclient_mock) -> None: assert state.attributes[ATTR_WEATHER_VISIBILITY] == 50 assert state.attributes[ATTR_WEATHER_WIND_SPEED] == 7 assert state.attributes[ATTR_WEATHER_WIND_BEARING] == 134 - _LOGGER.error(state.attributes) assert len(state.attributes["forecast"]) == 4 forecast = state.attributes["forecast"][1] @@ -81,157 +77,171 @@ async def test_setup_hass(hass: HomeAssistant, aioclient_mock) -> None: assert forecast[ATTR_FORECAST_CONDITION] == "partlycloudy" -def test_properties_no_data(hass: HomeAssistant) -> None: +async def test_properties_no_data(hass: HomeAssistant) -> None: """Test properties when no API data available.""" - weather = weather_smhi.SmhiWeather("name", "10", "10") - weather.hass = hass + entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) + entry.add_to_hass(hass) - assert weather.name == "name" - assert weather.should_poll is True - assert weather.temperature is None - assert weather.humidity is None - assert weather.wind_speed is None - assert weather.wind_gust_speed is None - assert weather.wind_bearing is None - assert weather.visibility is None - assert weather.pressure is None - assert weather.cloudiness is None - assert weather.thunder_probability is None - assert weather.condition is None - assert weather.forecast is None - assert weather.temperature_unit == TEMP_CELSIUS + with patch( + "homeassistant.components.smhi.weather.Smhi.async_get_forecast", + side_effect=SmhiForecastException("boom"), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + + assert state + assert state.name == "test" + assert state.state == STATE_UNKNOWN + assert ( + state.attributes[ATTR_WEATHER_ATTRIBUTION] == "Swedish weather institute (SMHI)" + ) + assert ATTR_WEATHER_HUMIDITY not in state.attributes + assert ATTR_WEATHER_PRESSURE not in state.attributes + assert ATTR_WEATHER_TEMPERATURE not in state.attributes + assert ATTR_WEATHER_VISIBILITY not in state.attributes + assert ATTR_WEATHER_WIND_SPEED not in state.attributes + assert ATTR_WEATHER_WIND_BEARING not in state.attributes + assert ATTR_FORECAST not in state.attributes + assert ATTR_SMHI_CLOUDINESS not in state.attributes + assert ATTR_SMHI_THUNDER_PROBABILITY not in state.attributes + assert ATTR_SMHI_WIND_GUST_SPEED not in state.attributes -# pylint: disable=protected-access -def test_properties_unknown_symbol() -> None: +async def test_properties_unknown_symbol(hass: HomeAssistant) -> None: """Test behaviour when unknown symbol from API.""" - hass = Mock() - data = Mock() - data.temperature = 5 - data.mean_precipitation = 0.5 - data.total_precipitation = 1 - data.humidity = 5 - data.wind_speed = 10 - data.wind_gust_speed = 17 - data.wind_direction = 180 - data.horizontal_visibility = 6 - data.pressure = 1008 - data.cloudiness = 52 - data.thunder_probability = 41 - data.symbol = 100 # Faulty symbol - data.valid_time = datetime(2018, 1, 1, 0, 1, 2) + data = SmhiForecast( + temperature=5, + temperature_max=10, + temperature_min=0, + humidity=5, + pressure=1008, + thunder=0, + cloudiness=52, + precipitation=1, + wind_direction=180, + wind_speed=10, + horizontal_visibility=6, + wind_gust=1.5, + mean_precipitation=0.5, + total_precipitation=1, + symbol=100, # Faulty symbol + valid_time=datetime(2018, 1, 1, 0, 1, 2), + ) - data2 = Mock() - data2.temperature = 5 - data2.mean_precipitation = 0.5 - data2.total_precipitation = 1 - data2.humidity = 5 - data2.wind_speed = 10 - data2.wind_gust_speed = 17 - data2.wind_direction = 180 - data2.horizontal_visibility = 6 - data2.pressure = 1008 - data2.cloudiness = 52 - data2.thunder_probability = 41 - data2.symbol = 100 # Faulty symbol - data2.valid_time = datetime(2018, 1, 1, 12, 1, 2) + data2 = SmhiForecast( + temperature=5, + temperature_max=10, + temperature_min=0, + humidity=5, + pressure=1008, + thunder=0, + cloudiness=52, + precipitation=1, + wind_direction=180, + wind_speed=10, + horizontal_visibility=6, + wind_gust=1.5, + mean_precipitation=0.5, + total_precipitation=1, + symbol=100, # Faulty symbol + valid_time=datetime(2018, 1, 1, 12, 1, 2), + ) - data3 = Mock() - data3.temperature = 5 - data3.mean_precipitation = 0.5 - data3.total_precipitation = 1 - data3.humidity = 5 - data3.wind_speed = 10 - data3.wind_gust_speed = 17 - data3.wind_direction = 180 - data3.horizontal_visibility = 6 - data3.pressure = 1008 - data3.cloudiness = 52 - data3.thunder_probability = 41 - data3.symbol = 100 # Faulty symbol - data3.valid_time = datetime(2018, 1, 2, 12, 1, 2) + data3 = SmhiForecast( + temperature=5, + temperature_max=10, + temperature_min=0, + humidity=5, + pressure=1008, + thunder=0, + cloudiness=52, + precipitation=1, + wind_direction=180, + wind_speed=10, + horizontal_visibility=6, + wind_gust=1.5, + mean_precipitation=0.5, + total_precipitation=1, + symbol=100, # Faulty symbol + valid_time=datetime(2018, 1, 2, 12, 1, 2), + ) testdata = [data, data2, data3] - weather = weather_smhi.SmhiWeather("name", "10", "10") - weather.hass = hass - weather._forecasts = testdata - assert weather.condition is None - forecast = weather.forecast[0] - assert forecast[ATTR_FORECAST_CONDITION] is None + entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.smhi.weather.Smhi.async_get_forecast", + return_value=testdata, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + + assert state + assert state.name == "test" + assert state.state == STATE_UNKNOWN + assert ATTR_FORECAST in state.attributes + assert all( + forecast[ATTR_FORECAST_CONDITION] is None + for forecast in state.attributes[ATTR_FORECAST] + ) -# pylint: disable=protected-access -async def test_refresh_weather_forecast_exceeds_retries(hass) -> None: +@pytest.mark.parametrize("error", [SmhiForecastException(), asyncio.TimeoutError()]) +async def test_refresh_weather_forecast_retry( + hass: HomeAssistant, error: Exception +) -> None: """Test the refresh weather forecast function.""" + entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) + entry.add_to_hass(hass) + now = utcnow() - with patch.object( - hass.helpers.event, "async_call_later" - ) as call_later, patch.object( - weather_smhi.SmhiWeather, - "get_weather_forecast", - side_effect=SmhiForecastException(), - ): + with patch( + "homeassistant.components.smhi.weather.Smhi.async_get_forecast", + side_effect=error, + ) as mock_get_forecast: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() - weather = weather_smhi.SmhiWeather("name", "17.0022", "62.0022") - weather.hass = hass - weather._fail_count = 2 + state = hass.states.get(ENTITY_ID) - await weather.async_update() - assert weather._forecasts is None - assert not call_later.mock_calls + assert state + assert state.name == "test" + assert state.state == STATE_UNKNOWN + assert mock_get_forecast.call_count == 1 + future = now + timedelta(seconds=RETRY_TIMEOUT + 1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() -async def test_refresh_weather_forecast_timeout(hass) -> None: - """Test timeout exception.""" - weather = weather_smhi.SmhiWeather("name", "17.0022", "62.0022") - weather.hass = hass + state = hass.states.get(ENTITY_ID) + assert state + assert state.state == STATE_UNKNOWN + assert mock_get_forecast.call_count == 2 - with patch.object( - hass.helpers.event, "async_call_later" - ) as call_later, patch.object( - weather_smhi.SmhiWeather, "retry_update" - ), patch.object( - weather_smhi.SmhiWeather, - "get_weather_forecast", - side_effect=asyncio.TimeoutError, - ): + future = future + timedelta(seconds=RETRY_TIMEOUT + 1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - await weather.async_update() - assert len(call_later.mock_calls) == 1 - # Assert we are going to wait RETRY_TIMEOUT seconds - assert call_later.mock_calls[0][1][0] == weather_smhi.RETRY_TIMEOUT + state = hass.states.get(ENTITY_ID) + assert state + assert state.state == STATE_UNKNOWN + assert mock_get_forecast.call_count == 3 + future = future + timedelta(seconds=RETRY_TIMEOUT + 1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() -async def test_refresh_weather_forecast_exception() -> None: - """Test any exception.""" - - hass = Mock() - weather = weather_smhi.SmhiWeather("name", "17.0022", "62.0022") - weather.hass = hass - - with patch.object( - hass.helpers.event, "async_call_later" - ) as call_later, patch.object( - weather, - "get_weather_forecast", - side_effect=SmhiForecastException(), - ): - await weather.async_update() - assert len(call_later.mock_calls) == 1 - # Assert we are going to wait RETRY_TIMEOUT seconds - assert call_later.mock_calls[0][1][0] == weather_smhi.RETRY_TIMEOUT - - -async def test_retry_update(): - """Test retry function of refresh forecast.""" - hass = Mock() - weather = weather_smhi.SmhiWeather("name", "17.0022", "62.0022") - weather.hass = hass - - with patch.object(weather, "async_update", AsyncMock()) as update: - await weather.retry_update(None) - assert len(update.mock_calls) == 1 + state = hass.states.get(ENTITY_ID) + assert state + assert state.state == STATE_UNKNOWN + # after three failed retries we stop retrying and go back to normal interval + assert mock_get_forecast.call_count == 3 def test_condition_class(): @@ -239,7 +249,7 @@ def test_condition_class(): def get_condition(index: int) -> str: """Return condition given index.""" - return [k for k, v in weather_smhi.CONDITION_CLASSES.items() if index in v][0] + return [k for k, v in CONDITION_CLASSES.items() if index in v][0] # SMHI definitions as follows, see # http://opendata.smhi.se/apidocs/metfcst/parameters.html