Skip to content

Commit

Permalink
Add example tests (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
raman325 authored Jan 13, 2021
1 parent f85cbb0 commit e059bc7
Show file tree
Hide file tree
Showing 18 changed files with 454 additions and 6 deletions.
26 changes: 25 additions & 1 deletion .github/workflows/pull.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,28 @@ jobs:
with:
python-version: "3.x"
- run: python3 -m pip install black
- run: black .
- run: black .

tests:
runs-on: "ubuntu-latest"
name: Run tests
steps:
- name: Check out code from GitHub
uses: "actions/checkout@v2"
- name: Setup Python
uses: "actions/setup-python@v1"
with:
python-version: "3.8"
- name: Install requirements
run: python3 -m pip install -r requirements_test.txt
- name: Run tests
run: |
pytest \
-qq \
--timeout=9 \
--durations=10 \
-n auto \
--cov custom_components.integration_blueprint \
-o console_output_style=count \
-p no:sugar \
tests
26 changes: 25 additions & 1 deletion .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,28 @@ jobs:
with:
python-version: "3.x"
- run: python3 -m pip install black
- run: black .
- run: black .

tests:
runs-on: "ubuntu-latest"
name: Run tests
steps:
- name: Check out code from GitHub
uses: "actions/checkout@v2"
- name: Setup Python
uses: "actions/setup-python@v1"
with:
python-version: "3.8"
- name: Install requirements
run: python3 -m pip install -r requirements_test.txt
- name: Run tests
run: |
pytest \
-qq \
--timeout=9 \
--durations=10 \
-n auto \
--cov custom_components.integration_blueprint \
-o console_output_style=count \
-p no:sugar \
tests
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
__pycache__
pythonenv*
pythonenv*
venv
.venv
.coverage
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.pythonPath": "/usr/local/bin/python"
"python.pythonPath": "/usr/local/bin/python",
"files.associations": {
"*.yaml": "home-assistant"
}
}
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,20 @@ File | Purpose
`custom_components/integration_blueprint/manifest.json` | A [manifest file](https://developers.home-assistant.io/docs/en/creating_integration_manifest.html) for Home Assistant.
`custom_components/integration_blueprint/sensor.py` | Sensor platform for the integration.
`custom_components/integration_blueprint/switch.py` | Switch sensor platform for the integration.
`tests/__init__.py` | Makes the `tests` folder a module.
`tests/conftest.py` | Global [fixtures](https://docs.pytest.org/en/stable/fixture.html) used in tests to [patch](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch) functions.
`tests/test_api.py` | Tests for `custom_components/integration_blueprint/api.py`.
`tests/test_config_flow.py` | Tests for `custom_components/integration_blueprint/config_flow.py`.
`tests/test_init.py` | Tests for `custom_components/integration_blueprint/__init__.py`.
`tests/test_switch.py` | Tests for `custom_components/integration_blueprint/switch.py`.
`CONTRIBUTING.md` | Guidelines on how to contribute.
`example.png` | Screenshot that demonstrate how it might look in the UI.
`info.md` | An example on a info file (used by [hacs][hacs]).
`LICENSE` | The license file for the project.
`README.md` | The file you are reading now, should contain info about the integration, installation and configuration instructions.
`requirements.txt` | Python packages used by this integration.
`requirements_dev.txt` | Python packages used to provide [IntelliSense](https://code.visualstudio.com/docs/editor/intellisense)/code hints during development of this integration, typically includes packages in `requirements.txt` but may include additional packages
`requirements_text.txt` | Python packages required to run the tests for this integration, typically includes packages in `requirements_dev.txt` but may include additional packages

## How?

Expand Down
1 change: 1 addition & 0 deletions custom_components/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Custom components module."""
2 changes: 1 addition & 1 deletion custom_components/integration_blueprint/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,4 @@ async def api_wrapper(
exception,
)
except Exception as exception: # pylint: disable=broad-except
_LOGGER.error("Something really wrong happend! - %s", exception)
_LOGGER.error("Something really wrong happened! - %s", exception)
1 change: 1 addition & 0 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
homeassistant
2 changes: 2 additions & 0 deletions requirements_test.txt
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
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@ not_skip = __init__.py
force_sort_within_sections = true
sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
default_section = THIRDPARTY
known_first_party = custom_components.integration_blueprint
known_first_party = custom_components.integration_blueprint, tests
combine_as_imports = true
24 changes: 24 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Why?

While tests aren't required to publish a custom component for Home Assistant, they will generally make development easier because good tests will expose when changes you want to make to the component logic will break expected functionality. Home Assistant uses [`pytest`](https://docs.pytest.org/en/latest/) for its tests, and the tests that have been included are modeled after tests that are written for core Home Assistant integrations. These tests pass with 100% coverage (unless something has changed ;) ) and have comments to help you understand the purpose of different parts of the test.

# Getting Started

To begin, it is recommended to create a virtual environment to install dependencies:
```bash
python3 -m venv venv
source venv/bin/activate
```

You can then install the dependencies that will allow you to run tests:
`pip3 install -r requirements_test.txt.`

This will install `homeassistant`, `pytest`, and `pytest-homeassistant-custom-component`, a plugin which allows you to leverage helpers that are available in Home Assistant for core integration tests.

# Useful commands

Command | Description
------- | -----------
`pytest tests/` | This will run all tests in `tests/` and tell you how many passed/failed
`pytest --durations=10 --cov-report term-missing --cov=custom_components.integration_blueprint tests` | This tells `pytest` that your target module to test is `custom_components.integration_blueprint` so that it can give you a [code coverage](https://en.wikipedia.org/wiki/Code_coverage) summary, including % of code that was executed and the line numbers of missed executions.
`pytest tests/test_init.py -k test_setup_unload_and_reload_entry` | Runs the `test_setup_unload_and_reload_entry` test function located in `tests/test_init.py`
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for integration_blueprint integration."""
56 changes: 56 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Global fixtures for integration_blueprint integration."""
# Fixtures allow you to replace functions with a Mock object. You can perform
# many options via the Mock to reflect a particular behavior from the original
# function that you want to see without going through the function's actual logic.
# Fixtures can either be passed into tests as parameters, or if autouse=True, they
# will automatically be used across all tests.
#
# Fixtures that are defined in conftest.py are available across all tests. You can also
# define fixtures within a particular test file to scope them locally.
#
# pytest_homeassistant_custom_component provides some fixtures that are provided by
# Home Assistant core. You can find those fixture definitions here:
# https://github.com/MatthewFlamm/pytest-homeassistant-custom-component/blob/master/pytest_homeassistant_custom_component/common.py
#
# See here for more info: https://docs.pytest.org/en/latest/fixture.html (note that
# pytest includes fixtures OOB which you can use as defined on this page)
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.integration_blueprint.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(
"custom_components.integration_blueprint.IntegrationBlueprintApiClient.async_get_data",
side_effect=Exception,
):
yield
5 changes: 5 additions & 0 deletions tests/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Constants for integration_blueprint tests."""
from custom_components.integration_blueprint.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"}
86 changes: 86 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Tests for integration_blueprint api."""
import asyncio

import aiohttp
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from custom_components.integration_blueprint.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_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]
)
Loading

0 comments on commit e059bc7

Please sign in to comment.