From a3763213c67e6f07378466010c79bfb32558bf82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Sun, 21 Jan 2024 20:06:48 +0100 Subject: [PATCH] Enable unit tests in GitHub workflows (#355) --- .github/workflows/tests.yaml | 55 +++++++++++-------- .vscode/launch.json | 17 ++++++ tests/conftest.py => conftest.py | 7 ++- custom_components/rct_power/__init__.py | 23 ++++---- .../rct_power/init_test.py | 54 +++++++++--------- poetry.lock | 30 +++++++++- pyproject.toml | 5 +- 7 files changed, 127 insertions(+), 64 deletions(-) create mode 100644 .vscode/launch.json rename tests/conftest.py => conftest.py (89%) rename tests/test_init.py => custom_components/rct_power/init_test.py (58%) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 4c439a3..b12f171 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -1,4 +1,4 @@ -name: Linting +name: "Linting and Testing" on: push: @@ -29,7 +29,7 @@ jobs: - name: Install Python modules run: | - poetry install + poetry install --no-root - name: Run pre-commit on all files run: | @@ -56,25 +56,32 @@ jobs: - name: Hassfest validation uses: "home-assistant/actions/hassfest@master" - # tests: - # runs-on: "ubuntu-latest" - # name: Run tests - # steps: - # - name: Check out code from GitHub - # uses: "actions/checkout@v4" - # - name: Setup Python - # uses: "actions/setup-python@v5" - # with: - # python-version-file: pyproject.toml - # - name: Install requirements - # run: | - # pip install --constraint=.github/workflows/constraints.txt pip - # pip install -r requirements_test.txt - # - name: Tests suite - # run: | - # pytest \ - # --timeout=9 \ - # --durations=10 \ - # -n auto \ - # -p no:sugar \ - # tests + + test: + runs-on: "ubuntu-latest" + name: "Test" + steps: + - name: Check out the repository + uses: "actions/checkout@v4" + + - name: Install poetry + run: pipx install poetry + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: pyproject.toml + cache: "poetry" + + - name: Install Python modules + run: | + poetry install --no-root + + - name: Run pytest + uses: pavelzw/pytest-action@v2 + with: + custom-pytest: "poetry run pytest" + verbose: true + emoji: true + job-summary: true + click-to-expand: false diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b06d97e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Debug Tests", + "type": "python", + "request": "launch", + "program": "${file}", + "purpose": ["debug-test"], + "console": "integratedTerminal", + "justMyCode": false + } + ] +} diff --git a/tests/conftest.py b/conftest.py similarity index 89% rename from tests/conftest.py rename to conftest.py index 6e482c5..f13b997 100644 --- a/tests/conftest.py +++ b/conftest.py @@ -3,7 +3,12 @@ import pytest -pytest_plugins = "pytest_homeassistant_custom_component" +pytest_plugins = ("pytest_homeassistant_custom_component",) + + +@pytest.fixture(autouse=True) +def auto_enable_custom_integrations(enable_custom_integrations: None): + yield # This fixture is used to prevent HomeAssistant from attempting to create and dismiss persistent diff --git a/custom_components/rct_power/__init__.py b/custom_components/rct_power/__init__.py index bd7c847..a7ff3aa 100644 --- a/custom_components/rct_power/__init__.py +++ b/custom_components/rct_power/__init__.py @@ -5,9 +5,12 @@ https://github.com/weltenwort/home-assistant-rct-power-integration """ import asyncio -from datetime import timedelta import logging -from typing import Any, Callable, cast +from datetime import timedelta +from typing import Any +from typing import Callable +from typing import cast +from typing import Literal from homeassistant.config_entries import ConfigEntry from homeassistant.core import Config @@ -15,16 +18,16 @@ from homeassistant.exceptions import ConfigEntryNotReady from .lib.api import RctPowerApiClient -from .lib.const import ( - DOMAIN, - PLATFORMS, - STARTUP_MESSAGE, -) +from .lib.const import DOMAIN +from .lib.const import PLATFORMS +from .lib.const import STARTUP_MESSAGE from .lib.context import RctPowerContext from .lib.domain_data import get_domain_data from .lib.entities import all_entity_descriptions -from .lib.entity import EntityUpdatePriority, resolve_object_infos -from .lib.entry import RctPowerConfigEntryData, RctPowerConfigEntryOptions +from .lib.entity import EntityUpdatePriority +from .lib.entity import resolve_object_infos +from .lib.entry import RctPowerConfigEntryData +from .lib.entry import RctPowerConfigEntryOptions from .lib.update_coordinator import RctPowerDataUpdateCoordinator SCAN_INTERVAL = timedelta(seconds=30) @@ -37,7 +40,7 @@ async def async_setup(hass: HomeAssistant, config: Config): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> Literal[True]: """Set up this integration using UI.""" if len(domain_data := get_domain_data(hass)) == 0: _LOGGER.info(STARTUP_MESSAGE) diff --git a/tests/test_init.py b/custom_components/rct_power/init_test.py similarity index 58% rename from tests/test_init.py rename to custom_components/rct_power/init_test.py index 1f73773..0f98aa2 100644 --- a/tests/test_init.py +++ b/custom_components/rct_power/init_test.py @@ -1,20 +1,13 @@ """Test RCT Power setup process.""" +from dataclasses import asdict + import pytest -from custom_components.rct_power import ( - async_reload_entry, -) -from custom_components.rct_power import ( - async_setup_entry, -) -from custom_components.rct_power import ( - async_unload_entry, -) -from custom_components.rct_power import ( - RctPowerDataUpdateCoordinator, -) -from custom_components.rct_power.lib.const import ( - DOMAIN, -) +from custom_components.rct_power import async_setup_entry +from custom_components.rct_power import async_unload_entry +from custom_components.rct_power.lib.const import DOMAIN +from custom_components.rct_power.lib.context import RctPowerContext +from custom_components.rct_power.lib.entry import RctPowerConfigEntryData +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from pytest_homeassistant_custom_component.common import MockConfigEntry @@ -24,35 +17,42 @@ # 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): +async def test_setup_unload_and_reload_entry( + hass: HomeAssistant, bypass_get_data: None +): """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={}, entry_id="test") + config_entry = MockConfigEntry( + domain=DOMAIN, + data=asdict(RctPowerConfigEntryData(hostname="localhost")), + entry_id="test", + ) + config_entry.add_to_hass(hass) # Set up the entry and assert that the values set during setup are where we expect # them to be. Because we have patched the RctPowerDataUpdateCoordinator.async_get_data # call, no code from custom_components/rct_power/api.py actually runs. - assert await async_setup_entry(hass, config_entry) + assert await hass.config_entries.async_setup(config_entry.entry_id) assert DOMAIN in hass.data and config_entry.entry_id in hass.data[DOMAIN] - assert isinstance( - hass.data[DOMAIN][config_entry.entry_id], RctPowerDataUpdateCoordinator - ) + assert isinstance(hass.data[DOMAIN][config_entry.entry_id], RctPowerContext) # Reload the entry and assert that the data from above is still there - assert await async_reload_entry(hass, config_entry) is None + assert await hass.config_entries.async_reload(config_entry.entry_id) assert DOMAIN in hass.data and config_entry.entry_id in hass.data[DOMAIN] - assert isinstance( - hass.data[DOMAIN][config_entry.entry_id], RctPowerDataUpdateCoordinator - ) + assert isinstance(hass.data[DOMAIN][config_entry.entry_id], RctPowerContext) # 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): +async def test_setup_entry_exception(hass: HomeAssistant, error_on_get_data: None): """Test ConfigEntryNotReady when API raises an exception during entry setup.""" - config_entry = MockConfigEntry(domain=DOMAIN, data={}, entry_id="test") + config_entry = MockConfigEntry( + domain=DOMAIN, + data=asdict(RctPowerConfigEntryData(hostname="localhost")), + 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 diff --git a/poetry.lock b/poetry.lock index bc463ad..0f5d985 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2185,6 +2185,20 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +[[package]] +name = "pytest-emoji" +version = "0.2.0" +description = "A pytest plugin that adds emojis to your test result report" +optional = false +python-versions = ">=3.4" +files = [ + {file = "pytest-emoji-0.2.0.tar.gz", hash = "sha256:e1bd4790d87649c2d09c272c88bdfc4d37c1cc7c7a46583087d7c510944571e8"}, + {file = "pytest_emoji-0.2.0-py3-none-any.whl", hash = "sha256:6e34ed21970fa4b80a56ad11417456bd873eb066c02315fe9df0fafe6d4d4436"}, +] + +[package.dependencies] +pytest = ">=4.2.1" + [[package]] name = "pytest-freezer" version = "0.4.8" @@ -2239,6 +2253,20 @@ sqlalchemy = "2.0.23" syrupy = "4.6.0" tqdm = "4.66.1" +[[package]] +name = "pytest-md" +version = "0.2.0" +description = "Plugin for generating Markdown reports for pytest results" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pytest-md-0.2.0.tar.gz", hash = "sha256:3b248d5b360ea5198e05b4f49c7442234812809a63137ec6cdd3643a40cf0112"}, + {file = "pytest_md-0.2.0-py3-none-any.whl", hash = "sha256:4c4cd16fea6d1485e87ee254558712c804a96d2aa9674b780e7eb8fb6526e1d1"}, +] + +[package.dependencies] +pytest = ">=4.2.1" + [[package]] name = "pytest-picked" version = "0.5.0" @@ -3211,4 +3239,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.13" -content-hash = "4f1a23199983734ccf480941bd1d3c6c4ab704a073a4ac33017d4e156661f996" +content-hash = "265605fbc7a043d16a318fa6cf340273f40b855499a872d261111e1f84fd05c1" diff --git a/pyproject.toml b/pyproject.toml index acc2426..85966c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,10 @@ black = "^23.12.1" flake8 = "^7.0.0" homeassistant-stubs = "^2024.1.3" pre-commit = "^3.3.3" +pytest = "^7.4.3" +pytest-emoji = "^0.2.0" pytest-homeassistant-custom-component = "^0.13.89" +pytest-md = "^0.2.0" reorder-python-imports = "^3.12.0" [build-system] @@ -25,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.pyright] typeCheckingMode = "strict" -reportMissingTypeStubs = "warning" +reportMissingTypeStubs = "none" reportImportCycles = "warning" useLibraryCodeForTypes = true