← Back to skills
extension
Category: Development & EngineeringAPI key requirement unconfirmed

ha-testing

Write tests for Home Assistant integrations using pytest and the hass fixture. Use when mentioning test, pytest, testing, test coverage, mock, fixture, or preparing an integration for core submission.

personAuthor: jakexiaohubgithub

Testing Home Assistant Integrations

Tests are required for Bronze tier on the Integration Quality Scale. At minimum: config flow tests for success, connection failure, and auth failure.

Test Structure

tests/
├── conftest.py           # Shared fixtures
├── test_config_flow.py   # Config flow tests (REQUIRED)
├── test_init.py          # Setup/unload tests
├── test_sensor.py        # Entity tests
└── test_coordinator.py   # Coordinator tests

conftest.py

"""Fixtures for {Name} tests."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch

import pytest

from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME


@pytest.fixture(autouse=True)
def auto_enable_custom_integrations(enable_custom_integrations):
    """Load custom_components during tests (fixture provided by PHCC)."""
    yield


MOCK_CONFIG = {
    CONF_HOST: "192.168.1.100",
    CONF_USERNAME: "admin",
    CONF_PASSWORD: "password",
}

MOCK_DEVICE_INFO = {
    "serial": "ABC123",
    "name": "Test Device",
    "model": "Model X",
}

MOCK_DATA = {
    "devices": {
        "device_1": {
            "temperature": 22.5,
            "humidity": 45,
        },
    },
}


@pytest.fixture
def mock_client() -> Generator[AsyncMock]:
    with patch("custom_components.{domain}.MyClient", autospec=True) as mock:
        client = mock.return_value
        client.async_get_device_info = AsyncMock(return_value=MOCK_DEVICE_INFO)
        client.async_get_data = AsyncMock(return_value=MOCK_DATA)
        yield client


@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
    with patch(
        "custom_components.{domain}.async_setup_entry",
        return_value=True,
    ) as mock:
        yield mock

test_config_flow.py (REQUIRED)

These assertions assume the config flow catches client errors and maps them to the errors["base"] keys below. Mirror this mapping in async_step_user (see the ha-debugging skill for the errors["base"] = "unknown" fallback pattern):

try:
    await client.async_get_device_info()
except ConnectionError:
    errors["base"] = "cannot_connect"
except InvalidAuth:
    errors["base"] = "invalid_auth"
except Exception:  # noqa: BLE001
    errors["base"] = "unknown"
"""Test config flow."""
from unittest.mock import AsyncMock

from homeassistant import config_entries
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType

from custom_components.{domain}.const import DOMAIN
from custom_components.{domain}.exceptions import InvalidAuth

from .conftest import MOCK_CONFIG


async def test_user_flow_success(
    hass: HomeAssistant,
    mock_client: AsyncMock,
    mock_setup_entry: AsyncMock,
) -> None:
    """Test successful config flow."""
    result = await hass.config_entries.flow.async_init(
        DOMAIN, context={"source": config_entries.SOURCE_USER}
    )
    assert result["type"] is FlowResultType.FORM
    assert result["errors"] == {}

    result = await hass.config_entries.flow.async_configure(
        result["flow_id"], MOCK_CONFIG
    )
    assert result["type"] is FlowResultType.CREATE_ENTRY
    assert result["title"] == "Test Device"
    assert result["data"] == MOCK_CONFIG


async def test_user_flow_cannot_connect(
    hass: HomeAssistant,
    mock_client: AsyncMock,
    mock_setup_entry: AsyncMock,
) -> None:
    """Test connection failure."""
    mock_client.async_get_device_info.side_effect = ConnectionError

    result = await hass.config_entries.flow.async_init(
        DOMAIN, context={"source": config_entries.SOURCE_USER}
    )
    result = await hass.config_entries.flow.async_configure(
        result["flow_id"], MOCK_CONFIG
    )
    assert result["type"] is FlowResultType.FORM
    assert result["errors"] == {"base": "cannot_connect"}


async def test_user_flow_invalid_auth(
    hass: HomeAssistant,
    mock_client: AsyncMock,
    mock_setup_entry: AsyncMock,
) -> None:
    """Test auth failure."""
    mock_client.async_get_device_info.side_effect = InvalidAuth

    result = await hass.config_entries.flow.async_init(
        DOMAIN, context={"source": config_entries.SOURCE_USER}
    )
    result = await hass.config_entries.flow.async_configure(
        result["flow_id"], MOCK_CONFIG
    )
    assert result["type"] is FlowResultType.FORM
    assert result["errors"] == {"base": "invalid_auth"}

test_init.py

"""Test setup and unload."""
from unittest.mock import AsyncMock

from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant

from pytest_homeassistant_custom_component.common import MockConfigEntry

from custom_components.{domain}.const import DOMAIN

from .conftest import MOCK_CONFIG


async def test_setup_entry(hass: HomeAssistant, mock_client: AsyncMock) -> None:
    """Test successful setup."""
    entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG)
    entry.add_to_hass(hass)
    await hass.config_entries.async_setup(entry.entry_id)
    await hass.async_block_till_done()
    assert entry.state is ConfigEntryState.LOADED


async def test_unload_entry(hass: HomeAssistant, mock_client: AsyncMock) -> None:
    """Test successful unload."""
    entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG)
    entry.add_to_hass(hass)
    await hass.config_entries.async_setup(entry.entry_id)
    await hass.async_block_till_done()
    assert entry.state is ConfigEntryState.LOADED

    await hass.config_entries.async_unload(entry.entry_id)
    await hass.async_block_till_done()
    assert entry.state is ConfigEntryState.NOT_LOADED


async def test_setup_entry_not_ready(
    hass: HomeAssistant, mock_client: AsyncMock
) -> None:
    """Test setup fails when cannot connect."""
    mock_client.async_get_data.side_effect = ConnectionError

    entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG)
    entry.add_to_hass(hass)
    await hass.config_entries.async_setup(entry.entry_id)
    await hass.async_block_till_done()
    assert entry.state is ConfigEntryState.SETUP_RETRY

Running Tests

pytest-homeassistant-custom-component (PHACC) pins its own compatible pytest, pytest-asyncio, and supporting plugin versions, so install only PHACC (pinned to the release matching your target HA version) and let it pull the stack — do not also install pytest-asyncio separately, as that invites version conflicts. Pin the version, e.g. pip install pytest-homeassistant-custom-component==<version-for-your-HA>.

The bare async def test_* examples above only run with PHACC's asyncio config. Add this minimal config so async tests are collected (otherwise pytest skips them with "async def functions are not natively supported"):

# pyproject.toml
[tool.pytest.ini_options]
asyncio_mode = "auto"

(Or the equivalent [pytest] / asyncio_mode = auto in pytest.ini.)

pip install pytest-homeassistant-custom-component==<version-for-your-HA>

pytest tests/ -v
pytest tests/ --cov=custom_components.{domain} --cov-report=html

Key Rules

  1. Config flow tests are mandatory for Bronze tier
  2. Use AsyncMock for all async methods
  3. Mock the client, not the coordinator
  4. Call await hass.async_block_till_done() after setup
  5. Assert FlowResultType enum values, not strings