Skip to content

Commit

Permalink
add tests using ludeeus/integration_blueprint#50 as base
Browse files Browse the repository at this point in the history
  • Loading branch information
raman325 committed Jan 6, 2021
1 parent b48bb70 commit c6c532b
Show file tree
Hide file tree
Showing 10 changed files with 283 additions and 1 deletion.
1 change: 1 addition & 0 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
homeassistant
3 changes: 3 additions & 0 deletions requirements_test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-r requirements_dev.txt
pytest
pytest-homeassistant-custom-component
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for {{cookiecutter.project_name}} integration."""
35 changes: 35 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Global fixtures for {{cookiecutter.project_name}} integration."""
from unittest import mock

import pytest
from pytest_homeassistant_custom_component.async_mock import patch

pytest_plugins = "pytest_homeassistant_custom_component"


@pytest.fixture(name="skip_notifications", autouse=True)
def skip_notifications_fixture():
"""Skip notification calls."""
with patch("homeassistant.components.persistent_notification.async_create"), patch(
"homeassistant.components.persistent_notification.async_dismiss"
):
yield


@pytest.fixture(name="bypass_get_data")
def bypass_get_data_fixture():
"""Skip calls to get data from API."""
with patch(
"custom_components.{{cookiecutter.project_name}}.{{cookiecutter.class_name_prefix}}ApiClient.async_get_data"
):
yield


@pytest.fixture(name="error_on_get_data")
def error_get_data_fixture():
"""Simulate error when retrieving data from API."""
with patch(
"custom_components.{{cookiecutter.project_name}}.{{cookiecutter.class_name_prefix}}ApiClient.async_get_data",
side_effect=Exception,
):
yield
4 changes: 4 additions & 0 deletions tests/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Constants for {{cookiecutter.project_name}} tests."""
from custom_components.{{cookiecutter.project_name}}.const import CONF_PASSWORD, CONF_USERNAME

MOCK_CONFIG = {CONF_USERNAME: "test_username", CONF_PASSWORD: "test_password"}
69 changes: 69 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Tests for {{cookiecutter.project_name}} api."""
import asyncio

import aiohttp
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from custom_components.{{cookiecutter.project_name}}.api import {{cookiecutter.class_name_prefix}}ApiClient


async def test_ap(hass, aioclient_mock, caplog):
"""Test API calls."""

api = {{cookiecutter.class_name_prefix}}ApiClient("test", "test", async_get_clientsession(hass))

aioclient_mock.get(
"https://jsonplaceholder.typicode.com/posts/1", json={"test": "test"}
)
assert await api.async_get_data() == {"test": "test"}

aioclient_mock.patch("https://jsonplaceholder.typicode.com/posts/1")
assert await api.async_set_title("test") is None

caplog.clear()
aioclient_mock.put(
"https://jsonplaceholder.typicode.com/posts/1", exc=asyncio.TimeoutError
)
assert (
await api.api_wrapper("put", "https://jsonplaceholder.typicode.com/posts/1")
is None
)
assert (
len(caplog.record_tuples) == 1
and "Timeout error fetching information from" in caplog.record_tuples[0][2]
)

caplog.clear()
aioclient_mock.post(
"https://jsonplaceholder.typicode.com/posts/1", exc=aiohttp.ClientError
)
assert (
await api.api_wrapper("post", "https://jsonplaceholder.typicode.com/posts/1")
is None
)
assert (
len(caplog.record_tuples) == 1
and "Error fetching information from" in caplog.record_tuples[0][2]
)

caplog.clear()
aioclient_mock.post("https://jsonplaceholder.typicode.com/posts/2", exc=Exception)
assert (
await api.api_wrapper("post", "https://jsonplaceholder.typicode.com/posts/2")
is None
)
assert (
len(caplog.record_tuples) == 1
and "Something really wrong happened!" in caplog.record_tuples[0][2]
)

caplog.clear()
aioclient_mock.post("https://jsonplaceholder.typicode.com/posts/3", exc=TypeError)
assert (
await api.api_wrapper("post", "https://jsonplaceholder.typicode.com/posts/3")
is None
)
assert (
len(caplog.record_tuples) == 1
and "Error parsing information from" in caplog.record_tuples[0][2]
)
70 changes: 70 additions & 0 deletions tests/test_config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Test {{cookiecutter.project_name}} config flow."""
from homeassistant import config_entries, data_entry_flow
from pytest_homeassistant_custom_component.common import MockConfigEntry

from custom_components.{{cookiecutter.project_name}}.const import (
BINARY_SENSOR,
DOMAIN,
PLATFORMS,
SENSOR,
SWITCH,
)

from .const import MOCK_CONFIG


async def test_successful_config_flow(hass, bypass_get_data):
"""Test a successful config flow."""
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_CREATE_ENTRY
assert result["title"] == "test_username"
assert result["data"] == MOCK_CONFIG
assert result["result"]


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"}


async def test_options_flow(hass):
"""Test an options flow."""
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test")
entry.add_to_hass(hass)
result = await hass.config_entries.options.async_init(entry.entry_id)

assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"

result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={platform: platform != SENSOR for platform in PLATFORMS},
)

assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "test_username"

assert entry.options == {BINARY_SENSOR: True, SENSOR: False, SWITCH: True}
56 changes: 56 additions & 0 deletions tests/test_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Test {{cookiecutter.project_name}} setup process."""
from homeassistant.exceptions import ConfigEntryNotReady
import pytest
from pytest_homeassistant_custom_component.common import MockConfigEntry

from custom_components.{{cookiecutter.project_name}} import (
{{cookiecutter.class_name_prefix}}DataUpdateCoordinator,
async_reload_entry,
async_setup_entry,
async_unload_entry,
)
from custom_components.{{cookiecutter.project_name}}.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 {{cookiecutter.class_name_prefix}}DataUpdateCoordinator.async_get_data
# call, no code from custom_components/{{cookiecutter.project_name}}/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 (
type(hass.data[DOMAIN][config_entry.entry_id]) == {{cookiecutter.class_name_prefix}}DataUpdateCoordinator
)

# 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 (
type(hass.data[DOMAIN][config_entry.entry_id]) == {{cookiecutter.class_name_prefix}}DataUpdateCoordinator
)

# 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)
43 changes: 43 additions & 0 deletions tests/test_switch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Test {{cookiecutter.project_name}} switch."""
from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON
from homeassistant.const import ATTR_ENTITY_ID
from pytest_homeassistant_custom_component.async_mock import call, patch
from pytest_homeassistant_custom_component.common import MockConfigEntry

from custom_components.{{cookiecutter.project_name}} import async_setup_entry
from custom_components.{{cookiecutter.project_name}}.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.{{cookiecutter.project_name}}.{{cookiecutter.class_name_prefix}}ApiClient.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")
2 changes: 1 addition & 1 deletion {{cookiecutter.project_name}}/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
[![Discord][discord-shield]][discord]
[![Community Forum][forum-shield]][forum]

**TO BE REMOVED: If you need help, as a developper, to use this custom component tempalte,
**TO BE REMOVED: If you need help, as a developer, to use this custom component tempalte,
please look at the [User Guide in the Cookiecutter documentation](https://cookiecutter-homeassistant-custom-component.readthedocs.io/en/stable/quickstart.html)**

**This component will set up the following platforms.**
Expand Down

0 comments on commit c6c532b

Please sign in to comment.