From d4e2b1d6fae10a6509d97845c06739eb28f517d1 Mon Sep 17 00:00:00 2001 From: Andrey Khrolenok Date: Sat, 6 Mar 2021 01:17:47 +0300 Subject: [PATCH] Add unit tests --- .devcontainer/configuration.yaml | 9 +- custom_components/car_wash/binary_sensor.py | 78 ++++--- custom_components/car_wash/const.py | 33 +-- tests/conftest.py | 22 -- tests/const.py | 4 - tests/fixtures/.gitkeep | 0 tests/test_api.py | 89 -------- tests/test_binary_sensor.py | 222 ++++++++++++++++++++ tests/test_config_flow.py | 110 ---------- tests/test_init.py | 56 ----- tests/test_switch.py | 44 ---- 11 files changed, 297 insertions(+), 370 deletions(-) delete mode 100644 tests/fixtures/.gitkeep delete mode 100644 tests/test_api.py create mode 100644 tests/test_binary_sensor.py delete mode 100644 tests/test_config_flow.py delete mode 100644 tests/test_init.py delete mode 100644 tests/test_switch.py diff --git a/.devcontainer/configuration.yaml b/.devcontainer/configuration.yaml index 53b8c09..96b29dd 100644 --- a/.devcontainer/configuration.yaml +++ b/.devcontainer/configuration.yaml @@ -3,7 +3,14 @@ default_config: logger: default: info logs: - custom_components.integration_blueprint: debug + custom_components.car_wash: debug # If you need to debug uncomment the line below (doc: https://www.home-assistant.io/integrations/debugpy/) # debugpy: + +binary_sensor: + - platform: car_wash + weather: weather.home_assistant + +weather: + - platform: demo diff --git a/custom_components/car_wash/binary_sensor.py b/custom_components/car_wash/binary_sensor.py index 4678cc9..a849cd9 100644 --- a/custom_components/car_wash/binary_sensor.py +++ b/custom_components/car_wash/binary_sensor.py @@ -1,22 +1,22 @@ -# -# Copyright (c) 2019, Andrey "Limych" Khrolenok +# Copyright (c) 2019-2021, Andrey "Limych" Khrolenok # Creative Commons BY-NC-SA 4.0 International Public License # (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/) -# """ The Car Wash binary sensor. For more details about this platform, please refer to the documentation at https://github.com/Limych/ha-car_wash/ """ + import logging from datetime import datetime +from typing import Optional import voluptuous as vol try: from homeassistant.components.binary_sensor import BinarySensorEntity -except ImportError: +except ImportError: # pragma: no cover from homeassistant.components.binary_sensor import ( BinarySensorDevice as BinarySensorEntity, ) @@ -44,21 +44,24 @@ CONF_WEATHER, DEFAULT_DAYS, DEFAULT_NAME, + DOMAIN, + ICON, STARTUP_MESSAGE, ) _LOGGER = logging.getLogger(__name__) + PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( { vol.Required(CONF_WEATHER): cv.entity_id, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_DAYS, default=DEFAULT_DAYS): vol.Coerce(int), + vol.Optional(CONF_DAYS, default=DEFAULT_DAYS): cv.positive_int, } ) -# pylint: disable=w0613 +# pylint: disable=unused-argument async def async_setup_platform( hass: HomeAssistant, config, async_add_entities, discovery_info=None ): @@ -84,26 +87,10 @@ def __init__(self, hass: HomeAssistant, friendly_name: str, weather_entity, days self._days = days self._state = None - async def async_added_to_hass(self): - """Register callbacks.""" - - @callback - def sensor_state_listener( # pylint: disable=w0613 - entity, old_state, new_state - ): - """Handle device state changes.""" - self.async_schedule_update_ha_state(True) - - @callback - def sensor_startup(event): # pylint: disable=w0613 - """Update template on startup.""" - async_track_state_change( - self._hass, [self._weather_entity], sensor_state_listener - ) - - self.async_schedule_update_ha_state(True) - - self._hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, sensor_startup) + @property + def unique_id(self): + """Return a unique ID to use for this entity.""" + return DOMAIN + "-" + str(self._weather_entity).split(".")[1] @property def should_poll(self): @@ -115,6 +102,11 @@ def name(self): """Return the name of the sensor.""" return self._name + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._state is not None + @property def is_on(self): """Return True if sensor is on.""" @@ -123,10 +115,31 @@ def is_on(self): @property def icon(self): """Return the icon to use in the frontend, if any.""" - return "mdi:car-wash" + return ICON + + async def async_added_to_hass(self): + """Register callbacks.""" + + # pylint: disable=unused-argument + @callback + def sensor_state_listener(entity, old_state, new_state): + """Handle device state changes.""" + self.async_schedule_update_ha_state(True) + + # pylint: disable=unused-argument + @callback + def sensor_startup(event): + """Update template on startup.""" + async_track_state_change( + self._hass, [self._weather_entity], sensor_state_listener + ) + + self.async_schedule_update_ha_state(True) + + self._hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, sensor_startup) @staticmethod - def _temp2c(temperature: float, temperature_unit: str) -> float: + def _temp2c(temperature: Optional[float], temperature_unit: str) -> Optional[float]: """Convert weather temperature to Celsius degree.""" if temperature is not None and temperature_unit != TEMP_CELSIUS: temperature = convert_temperature( @@ -135,7 +148,8 @@ def _temp2c(temperature: float, temperature_unit: str) -> float: return temperature - async def async_update(self): # pylint: disable=r0912,r0915 + # pylint: disable=r0912,r0915 + async def async_update(self): """Update the sensor state.""" wdata = self._hass.states.get(self._weather_entity) @@ -150,6 +164,7 @@ async def async_update(self): # pylint: disable=r0912,r0915 forecast = wdata.attributes.get(ATTR_FORECAST) if forecast is None: + self._state = None raise HomeAssistantError( "Can't get forecast data! Are you sure it's the weather provider?" ) @@ -163,9 +178,10 @@ async def async_update(self): # pylint: disable=r0912,r0915 self._state = False return - cur_date = datetime.now().strftime("%F") + today = dt_util.start_of_local_day() + cur_date = today.strftime("%F") stop_date = datetime.fromtimestamp( - datetime.now().timestamp() + 86400 * (self._days + 1) + today.timestamp() + 86400 * (self._days + 1) ).strftime("%F") _LOGGER.debug("Inspect weather forecast from now till %s", stop_date) diff --git a/custom_components/car_wash/const.py b/custom_components/car_wash/const.py index e2c147b..a85704e 100644 --- a/custom_components/car_wash/const.py +++ b/custom_components/car_wash/const.py @@ -1,8 +1,6 @@ -# # Copyright (c) 2019-2021, Andrey "Limych" Khrolenok # Creative Commons BY-NC-SA 4.0 International Public License # (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/) -# """ The Car Wash binary sensor. @@ -10,6 +8,17 @@ https://github.com/Limych/ha-car_wash/ """ +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR +from homeassistant.components.weather import ( + ATTR_CONDITION_EXCEPTIONAL, + ATTR_CONDITION_HAIL, + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, +) + # Base component constants NAME = "Car Wash" DOMAIN = "car_wash" @@ -27,14 +36,12 @@ """ # Icons +ICON = "mdi:car-wash" # Device classes # Platforms -BINARY_SENSOR = "binary_sensor" -SENSOR = "sensor" -SWITCH = "switch" -PLATFORMS = [BINARY_SENSOR, SENSOR, SWITCH] +PLATFORMS = [BINARY_SENSOR] # Configuration and options CONF_WEATHER = "weather" @@ -48,11 +55,11 @@ BAD_CONDITIONS = [ - "lightning-rainy", - "rainy", - "pouring", - "snowy", - "snowy-rainy", - "hail", - "exceptional", + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_HAIL, + ATTR_CONDITION_EXCEPTIONAL, ] diff --git a/tests/conftest.py b/tests/conftest.py index dec8a8a..217086a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,8 +18,6 @@ import pytest -from custom_components.car_wash import IntegrationBlueprintApiClient - pytest_plugins = "pytest_homeassistant_custom_component" # pylint: disable=invalid-name @@ -33,23 +31,3 @@ def skip_notifications_fixture(): "homeassistant.components.persistent_notification.async_dismiss" ): yield - - -# This fixture, when used, will result in calls to async_get_data to return None. To have the call -# return a value, we would add the `return_value=` parameter to the patch call. -@pytest.fixture(name="bypass_get_data") -def bypass_get_data_fixture(): - """Skip calls to get data from API.""" - with patch.object(IntegrationBlueprintApiClient, "async_get_data"): - yield - - -# In this fixture, we are forcing calls to async_get_data to raise an Exception. This is useful -# for exception handling. -@pytest.fixture(name="error_on_get_data") -def error_get_data_fixture(): - """Simulate error when retrieving data from API.""" - with patch.object( - IntegrationBlueprintApiClient, "async_get_data", side_effect=Exception - ): - yield diff --git a/tests/const.py b/tests/const.py index 5fdba13..a8ec17e 100644 --- a/tests/const.py +++ b/tests/const.py @@ -1,5 +1 @@ """Constants for tests.""" -from custom_components.car_wash.const import CONF_PASSWORD, CONF_USERNAME - -# Mock config data to be used across multiple tests -MOCK_CONFIG = {CONF_USERNAME: "test_username", CONF_PASSWORD: "test_password"} diff --git a/tests/fixtures/.gitkeep b/tests/fixtures/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_api.py b/tests/test_api.py deleted file mode 100644 index b8c4e53..0000000 --- a/tests/test_api.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Tests for integration_blueprint api.""" -import asyncio - -import aiohttp -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from pytest import raises - -from custom_components.car_wash.api import IntegrationBlueprintApiClient - - -async def test_api(hass, aioclient_mock, caplog): - """Test API calls.""" - - # To test the api submodule, we first create an instance of our API client - api = IntegrationBlueprintApiClient("test", "test", async_get_clientsession(hass)) - - # Use aioclient_mock which is provided by `pytest_homeassistant_custom_component` - # to mock responses to aiohttp requests. In this case we are telling the mock to - # return {"test": "test"} when a `GET` call is made to the specified URL. We then - # call `async_get_data` which will make that `GET` request. - aioclient_mock.get( - "https://jsonplaceholder.typicode.com/posts/1", json={"test": "test"} - ) - assert await api.async_get_data() == {"test": "test"} - - # We do the same for `async_set_title`. Note the difference in the mock call - # between the previous step and this one. We use `patch` here instead of `get` - # because we know that `async_set_title` calls `api_wrapper` with `patch` as the - # first parameter - aioclient_mock.clear_requests() - # - aioclient_mock.patch("https://jsonplaceholder.typicode.com/posts/1") - assert await api.async_set_title("test") is None - - # In order to get 100% coverage, we need to test `api_wrapper` to test the code - # that isn't already called by `async_get_data` and `async_set_title`. Because the - # only logic that lives inside `api_wrapper` that is not being handled by a third - # party library (aiohttp) is the exception handling, we also want to simulate - # raising the exceptions to ensure that the function handles them as expected. - # The caplog fixture allows access to log messages in tests. This is particularly - # useful during exception handling testing since often the only action as part of - # exception handling is a logging statement - caplog.clear() - aioclient_mock.clear_requests() - # - aioclient_mock.put( - "https://jsonplaceholder.typicode.com/posts/1", exc=asyncio.TimeoutError - ) - with raises(asyncio.TimeoutError): - await api.api_wrapper("put", "https://jsonplaceholder.typicode.com/posts/1") - assert ( - len(caplog.record_tuples) == 1 - and "Timeout error fetching information from" in caplog.record_tuples[0][2] - ) - - caplog.clear() - aioclient_mock.clear_requests() - # - aioclient_mock.post("https://jsonplaceholder.typicode.com/posts/3", exc=TypeError) - with raises(TypeError): - await api.api_wrapper("post", "https://jsonplaceholder.typicode.com/posts/3") - assert ( - len(caplog.record_tuples) == 1 - and "Error parsing information from" in caplog.record_tuples[0][2] - ) - - caplog.clear() - aioclient_mock.clear_requests() - # - aioclient_mock.post( - "https://jsonplaceholder.typicode.com/posts/1", exc=aiohttp.ClientError - ) - with raises(aiohttp.ClientError): - await api.api_wrapper("post", "https://jsonplaceholder.typicode.com/posts/1") - assert ( - len(caplog.record_tuples) == 1 - and "Error fetching information from" in caplog.record_tuples[0][2] - ) - - caplog.clear() - aioclient_mock.clear_requests() - # - aioclient_mock.post("https://jsonplaceholder.typicode.com/posts/2", exc=Exception) - with raises(Exception): - await api.api_wrapper("post", "https://jsonplaceholder.typicode.com/posts/2") - assert ( - len(caplog.record_tuples) == 1 - and "Something really wrong happened!" in caplog.record_tuples[0][2] - ) diff --git a/tests/test_binary_sensor.py b/tests/test_binary_sensor.py new file mode 100644 index 0000000..6e519bd --- /dev/null +++ b/tests/test_binary_sensor.py @@ -0,0 +1,222 @@ +"""The test for the average sensor platform.""" +# pylint: disable=redefined-outer-name +import pytest +from homeassistant.components.weather import ( + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_FORECAST, + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_WEATHER_TEMPERATURE, +) +from homeassistant.const import ( + CONF_NAME, + CONF_PLATFORM, + STATE_OFF, + STATE_ON, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util +from pytest import raises +from pytest_homeassistant_custom_component.common import assert_setup_component + +from custom_components.car_wash.binary_sensor import CarWashBinarySensor +from custom_components.car_wash.const import CONF_WEATHER, DOMAIN, ICON + +TEST_CONFIG = { + CONF_PLATFORM: DOMAIN, + CONF_NAME: "test", + CONF_WEATHER: "weather.test_monitored", +} + + +@pytest.fixture() +async def mock_weather(hass: HomeAssistant): + """Mock weather entity.""" + assert await async_setup_component( + hass, + "weather", + { + "weather": { + "platform": "template", + "name": "test_monitored", + "condition_template": "{{ 0 }}", + "temperature_template": "{{ 0 }}", + "humidity_template": "{{ 0 }}", + } + }, + ) + await hass.async_block_till_done() + + +async def test_entity_initialization(hass: HomeAssistant): + """Test sensor initialization.""" + entity = CarWashBinarySensor(hass, "test", "weather.test_monitored", 2) + + assert entity.name == "test" + assert entity.unique_id == "car_wash-test_monitored" + assert entity.should_poll is False + assert entity.available is False + assert entity.is_on is None + assert entity.icon is ICON + + +async def test_async_setup_platform(hass: HomeAssistant, mock_weather): + """Test platform setup.""" + with assert_setup_component(1, "binary_sensor"): + assert await async_setup_component( + hass, + "binary_sensor", + { + "binary_sensor": TEST_CONFIG, + }, + ) + await hass.async_block_till_done() + + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state is not None + assert state.state == STATE_ON + + hass.states.async_set( + "weather.test_monitored", ATTR_CONDITION_RAINY, {ATTR_FORECAST: {}} + ) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state is not None + assert state.state == STATE_OFF + + +# pylint: disable=protected-access +async def test__temp2c(): + """Test temperature conversions.""" + assert CarWashBinarySensor._temp2c(10, TEMP_CELSIUS) == 10 + assert round(CarWashBinarySensor._temp2c(10, TEMP_FAHRENHEIT), 2) == -12.22 + assert CarWashBinarySensor._temp2c(None, TEMP_CELSIUS) is None + + +async def test_async_update(hass: HomeAssistant, mock_weather): + """Test platform setup.""" + entity = CarWashBinarySensor(hass, "test", "weather.nonexistent", 2) + with raises(HomeAssistantError): + await entity.async_update() + + entity = CarWashBinarySensor(hass, "test", "weather.test_monitored", 2) + assert entity.is_on is None + + hass.states.async_set("weather.test_monitored", None) + with raises(HomeAssistantError): + await entity.async_update() + + hass.states.async_set( + "weather.test_monitored", ATTR_CONDITION_RAINY, {ATTR_FORECAST: []} + ) + await entity.async_update() + assert entity.is_on is False + + today = dt_util.start_of_local_day() + today_ts = int(today.timestamp() * 1000) + day = days = 86400000 + + hass.states.async_set( + "weather.test_monitored", + ATTR_CONDITION_SUNNY, + { + ATTR_FORECAST: [ + { + ATTR_FORECAST_TIME: int(today_ts - day), + }, + { + ATTR_FORECAST_TIME: today, + }, + { + ATTR_FORECAST_TIME: int(today_ts + 3 * days), + }, + ] + }, + ) + await entity.async_update() + assert entity.is_on is True + + hass.states.async_set( + "weather.test_monitored", + ATTR_CONDITION_SUNNY, + { + ATTR_FORECAST: [ + { + ATTR_FORECAST_TIME: today, + ATTR_FORECAST_PRECIPITATION: 1, + }, + ] + }, + ) + await entity.async_update() + assert entity.is_on is False + + hass.states.async_set( + "weather.test_monitored", + ATTR_CONDITION_SUNNY, + { + ATTR_FORECAST: [ + { + ATTR_FORECAST_TIME: today, + ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY, + }, + ] + }, + ) + await entity.async_update() + assert entity.is_on is False + + hass.states.async_set( + "weather.test_monitored", + ATTR_CONDITION_SUNNY, + { + ATTR_WEATHER_TEMPERATURE: 0, + ATTR_FORECAST: [ + { + ATTR_FORECAST_TIME: today, + }, + { + ATTR_FORECAST_TIME: int(today_ts + day), + ATTR_FORECAST_TEMP_LOW: -1, + ATTR_FORECAST_TEMP: -1, + }, + { + ATTR_FORECAST_TIME: int(today_ts + 2 * days), + ATTR_FORECAST_TEMP_LOW: 1, + }, + ], + }, + ) + await entity.async_update() + assert entity.is_on is False + + hass.states.async_set( + "weather.test_monitored", + ATTR_CONDITION_SUNNY, + { + ATTR_WEATHER_TEMPERATURE: -1, + ATTR_FORECAST: [ + { + ATTR_FORECAST_TIME: today, + }, + { + ATTR_FORECAST_TIME: int(today_ts + day), + ATTR_FORECAST_TEMP: 1, + }, + ], + }, + ) + await entity.async_update() + assert entity.is_on is False diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py deleted file mode 100644 index ae599dd..0000000 --- a/tests/test_config_flow.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Test integration_blueprint config flow.""" -from unittest.mock import patch - -import pytest -from homeassistant import config_entries, data_entry_flow -from pytest_homeassistant_custom_component.common import MockConfigEntry - -from custom_components.car_wash.const import ( - BINARY_SENSOR, - DOMAIN, - PLATFORMS, - SENSOR, - SWITCH, -) - -from .const import MOCK_CONFIG - - -# This fixture bypasses the actual setup of the integration -# since we only want to test the config flow. We test the -# actual functionality of the integration in other test modules. -@pytest.fixture(autouse=True) -def bypass_setup_fixture(): - """Prevent setup.""" - with patch( - "custom_components.integration_blueprint.async_setup", - return_value=True, - ), patch( - "custom_components.integration_blueprint.async_setup_entry", - return_value=True, - ): - yield - - -# Here we simiulate a successful config flow from the backend. -# Note that we use the `bypass_get_data` fixture here because -# we want the config flow validation to succeed during the test. -async def test_successful_config_flow(hass, bypass_get_data): - """Test a successful config flow.""" - # Initialize a config flow - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - # Check that the config flow shows the user form as the first step - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - # If a user were to enter `test_username` for username and `test_password` - # for password, it would result in this function call - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_CONFIG - ) - - # Check that the config flow is complete and a new entry is created with - # the input data - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "test_username" - assert result["data"] == MOCK_CONFIG - assert result["result"] - - -# In this case, we want to simulate a failure during the config flow. -# We use the `error_on_get_data` mock instead of `bypass_get_data` -# (note the function parameters) to raise an Exception during -# validation of the input config. -async def test_failed_config_flow(hass, error_on_get_data): - """Test a failed config flow due to credential validation failure.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_CONFIG - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "auth"} - - -# Our config flow also has an options flow, so we must test it as well. -async def test_options_flow(hass): - """Test an options flow.""" - # Create a new MockConfigEntry and add to HASS (we're bypassing config - # flow entirely) - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test") - entry.add_to_hass(hass) - - # Initialize an options flow - result = await hass.config_entries.options.async_init(entry.entry_id) - - # Verify that the first options step is a user form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - # Enter some fake data into the form - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={platform: platform != SENSOR for platform in PLATFORMS}, - ) - - # Verify that the flow finishes - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "test_username" - - # Verify that the options were updated - assert entry.options == {BINARY_SENSOR: True, SENSOR: False, SWITCH: True} diff --git a/tests/test_init.py b/tests/test_init.py deleted file mode 100644 index 4e209ec..0000000 --- a/tests/test_init.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Test integration_blueprint setup process.""" -import pytest -from homeassistant.exceptions import ConfigEntryNotReady -from pytest_homeassistant_custom_component.common import MockConfigEntry - -from custom_components.car_wash import ( - BlueprintDataUpdateCoordinator, - async_reload_entry, - async_setup_entry, - async_unload_entry, -) -from custom_components.car_wash.const import DOMAIN - -from .const import MOCK_CONFIG - - -# We can pass fixtures as defined in conftest.py to tell pytest to use the fixture -# for a given test. We can also leverage fixtures and mocks that are available in -# Home Assistant using the pytest_homeassistant_custom_component plugin. -# Assertions allow you to verify that the return value of whatever is on the left -# side of the assertion matches with the right side. -async def test_setup_unload_and_reload_entry(hass, bypass_get_data): - """Test entry setup and unload.""" - # Create a mock entry so we don't have to go through config flow - config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test") - - # Set up the entry and assert that the values set during setup are where we expect - # them to be. Because we have patched the BlueprintDataUpdateCoordinator.async_get_data - # call, no code from custom_components/integration_blueprint/api.py actually runs. - assert await async_setup_entry(hass, config_entry) - assert DOMAIN in hass.data and config_entry.entry_id in hass.data[DOMAIN] - assert isinstance( - hass.data[DOMAIN][config_entry.entry_id], BlueprintDataUpdateCoordinator - ) - - # Reload the entry and assert that the data from above is still there - assert await async_reload_entry(hass, config_entry) is None - assert DOMAIN in hass.data and config_entry.entry_id in hass.data[DOMAIN] - assert isinstance( - hass.data[DOMAIN][config_entry.entry_id], BlueprintDataUpdateCoordinator - ) - - # Unload the entry and verify that the data has been removed - assert await async_unload_entry(hass, config_entry) - assert config_entry.entry_id not in hass.data[DOMAIN] - - -async def test_setup_entry_exception(hass, error_on_get_data): - """Test ConfigEntryNotReady when API raises an exception during entry setup.""" - config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test") - - # In this case we are testing the condition where async_setup_entry raises - # ConfigEntryNotReady using the `error_on_get_data` fixture which simulates - # an error. - with pytest.raises(ConfigEntryNotReady): - assert await async_setup_entry(hass, config_entry) diff --git a/tests/test_switch.py b/tests/test_switch.py deleted file mode 100644 index 3bc0209..0000000 --- a/tests/test_switch.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Test integration_blueprint switch.""" -from unittest.mock import call, patch - -from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON -from homeassistant.const import ATTR_ENTITY_ID -from pytest_homeassistant_custom_component.common import MockConfigEntry - -from custom_components.car_wash import async_setup_entry -from custom_components.car_wash.const import DEFAULT_NAME, DOMAIN, SWITCH - -from .const import MOCK_CONFIG - - -async def test_switch_services(hass): - """Test switch services.""" - # Create a mock entry so we don't have to go through config flow - config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test") - assert await async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - - # Functions/objects can be patched directly in test code as well and can be used to test - # additional things, like whether a function was called or what arguments it was called with - with patch( - "custom_components.integration_blueprint.IntegrationBlueprintApiClient.async_set_title" - ) as title_func: - await hass.services.async_call( - SWITCH, - SERVICE_TURN_OFF, - service_data={ATTR_ENTITY_ID: f"{SWITCH}.{DEFAULT_NAME}_{SWITCH}"}, - blocking=True, - ) - assert title_func.called - assert title_func.call_args == call("foo") - - title_func.reset_mock() - - await hass.services.async_call( - SWITCH, - SERVICE_TURN_ON, - service_data={ATTR_ENTITY_ID: f"{SWITCH}.{DEFAULT_NAME}_{SWITCH}"}, - blocking=True, - ) - assert title_func.called - assert title_func.call_args == call("bar")