-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add tests using ludeeus/integration_blueprint#50 as base (#112)
* add tests using ludeeus/integration_blueprint#50 as base * move tests folder to the right place * add test files to files table * try to fix * move requirements to appropriate folder * add a note to contributing about tests * add a note to contributing about tests * Update docs/guide.rst Co-authored-by: Oncleben31 <oncleben31@users.noreply.github.com> * Update {{cookiecutter.project_name}}/CONTRIBUTING.md Co-authored-by: Oncleben31 <oncleben31@users.noreply.github.com> * Update {{cookiecutter.project_name}}/tests/test_init.py Co-authored-by: Oncleben31 <oncleben31@users.noreply.github.com> * address review comments * add more comments * fix description for requirements files * add more comments * update gitignore and fix CONTRIBUTING * add dummy init file so that pytest works * Update {{cookiecutter.project_name}}/requirements_test.txt Co-authored-by: Oncleben31 <oncleben31@users.noreply.github.com> * fix import statements * Fix spelling * bypass setup in config flow * fix comment * incorporate comments that were in integration_blueprint PR * add tests to isort known_first_party Co-authored-by: Oncleben31 <oncleben31@users.noreply.github.com>
- Loading branch information
1 parent
b48bb70
commit b5c30b5
Showing
16 changed files
with
402 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,3 +3,6 @@ | |
__pycache__/ | ||
/.python-version | ||
/.vscode/ | ||
.coverage | ||
venv | ||
.venv |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
"""Dummy init so that pytest works.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
homeassistant |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
-r requirements_dev.txt | ||
pytest-homeassistant-custom-component==0.1.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
"""Tests for {{cookiecutter.friendly_name}} integration.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
"""Global fixtures for {{cookiecutter.friendly_name}} integration.""" | ||
from unittest.mock import patch | ||
|
||
import pytest | ||
|
||
pytest_plugins = "pytest_homeassistant_custom_component" | ||
|
||
# This fixture is used to prevent HomeAssistant from attempting to create and dismiss persistent | ||
# notifications. These calls would fail without this fixture since the persistent_notification | ||
# integration is never loaded during a test. | ||
@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 | ||
|
||
|
||
# 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=<VALUE_TO_RETURN>` 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( | ||
"custom_components.{{cookiecutter.domain_name}}.{{cookiecutter.class_name_prefix}}ApiClient.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( | ||
"custom_components.{{cookiecutter.domain_name}}.{{cookiecutter.class_name_prefix}}ApiClient.async_get_data", | ||
side_effect=Exception, | ||
): | ||
yield |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
"""Constants for {{cookiecutter.friendly_name}} tests.""" | ||
from custom_components.{{cookiecutter.domain_name}}.const import CONF_PASSWORD, CONF_USERNAME | ||
|
||
MOCK_CONFIG = {CONF_USERNAME: "test_username", CONF_PASSWORD: "test_password"} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
"""Tests for {{cookiecutter.friendly_name}} api.""" | ||
import asyncio | ||
|
||
import aiohttp | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
|
||
from custom_components.{{cookiecutter.domain_name}}.api import {{cookiecutter.class_name_prefix}}ApiClient | ||
|
||
|
||
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 = {{cookiecutter.class_name_prefix}}ApiClient("test", "test", async_get_clientsession(hass)) | ||
|
||
# Use aioclient_mock which is provided by `pytest_homeassistant_custom_components` | ||
# 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.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.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] | ||
) |
111 changes: 111 additions & 0 deletions
111
{{cookiecutter.project_name}}/tests/test_config_flow.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
"""Test {{cookiecutter.friendly_name}} 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.{{cookiecutter.domain_name}}.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.{{cookiecutter.domain_name}}.async_setup", | ||
return_value=True, | ||
), patch( | ||
"custom_components.{{cookiecutter.domain_name}}.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} |
Oops, something went wrong.