Home AssistantHome AssistantContributors
On this page

Every integration in core comes with tests, and every change to it comes with tests too. Tests prove that your change works, and more importantly, that it keeps working when someone else changes something next year. This page is about writing those tests. The commands to run them, check coverage, and update snapshots are on running the tests.

The test folder

Tests live in tests/components/<domain>/, next to the tests of all other integrations. The tests of the AirGradient integration look like this:

tests/components/airgradient/
├── __init__.py
├── conftest.py
├── fixtures/
├── snapshots/
├── test_button.py
├── test_config_flow.py
├── test_diagnostics.py
├── test_init.py
├── test_number.py
├── test_select.py
├── test_sensor.py
├── test_switch.py
└── test_update.py
File What it holds
__init__.py Needed for pytest to find the tests. A docstring is enough, like """Tests for the AirGradient integration.""".
conftest.py Pytest fixtures shared by the tests of this integration, like a mocked library client and a MockConfigEntry
fixtures/ Sample data from the device or service, like JSON responses
snapshots/ Snapshot files generated by the tests. Do not edit them by hand.
test_init.py Tests of __init__.py: setting up, reloading, and unloading a config entry, and setup failures
test_config_flow.py Tests of the config flow, options flow, and reauthentication and reconfiguration
test_<platform>.py One file per platform, like test_sensor.py
test_<feature>.py One file per other part of the integration, like test_diagnostics.py

The rule is simple: one test file per file of your integration, named after it with test_ in front.

Test through Home Assistant

Test your integration the way Home Assistant uses it: through its public interfaces, never by poking at the internals of the integration. Tests that work this way keep working when the integration is refactored.

Focused unit tests for a helper function or pure logic can call that code directly. That is fine, as long as the behavior of the integration is tested through Home Assistant.

Fixtures and mocking

Tests run without the real device or service. Shared setup goes into fixtures, in the conftest.py of the integration's tests. A typical integration has a fixture for the config entry, one that mocks the library, and one that sets it all up.

Mock the library your integration uses, not Home Assistant. Patch the client class of the library where your integration imports it, and give the mock the data you need:

Python
@pytest.fixture
def mock_example_client() -> Generator[MagicMock]:
    """Return a mocked Example client."""
    with patch(
        "homeassistant.components.example.coordinator.ExampleClient",
        autospec=True,
    ) as client_mock:
        client = client_mock.return_value
        client.get_status.return_value = Status(temperature=21.5)
        yield client

Mocking the coordinator, hass helpers, or entity internals makes tests brittle, and skips the code you actually want to test.

Put sample data, like a JSON response of the device, in the fixtures/ folder of your tests, and load it with the helpers in tests/common.py. load_fixture("status.json", DOMAIN) returns the text of tests/components/<domain>/fixtures/status.json. load_json_object_fixture and load_json_array_fixture parse it as JSON. In async code, use the async_ versions, like await async_load_json_object_fixture(hass, "status.json", DOMAIN): they read the file outside the event loop.

Core also has fixtures for the things you need often, like hass, entity_registry, device_registry, hass_ws_client for the WebSocket API, and issue_registry. Some more conventions:

  • Give every parameter of a test function a type annotation, with a concrete type like HomeAssistant or MockConfigEntry, not Any.
  • Use @pytest.mark.usefixtures for fixtures you need but do not use in the test body.
  • Use @pytest.mark.parametrize when several tests do the same thing with different inputs. Name the cases with pytest.param(..., id="name").
  • Avoid if statements in tests. Split the test, or parametrize it.
  • Use the freezer fixture for anything time related. Never patch datetime or utcnow yourself.
  • Hardcoded entity IDs are fine. If you repeat one, make it a constant.
  • Tests have no network access. Everything outside Home Assistant is mocked.

Sharing fixtures with other integrations

An entity integration, like light or sensor, can offer fixtures that other integrations use in their tests. For example, it can add fixture stubs to tests/components/conftest.py, and put the implementation in tests/components/<domain>/common.py.

Snapshot tests

A snapshot test compares the result of your code with a stored reference: the snapshot. It is a quick way to check large outputs, like the state of all entities, a diagnostics dump, or the entries in a registry. Core uses Syrupy for this.

To test every entity of a platform at once, use the snapshot_platform helper:

Python
async def test_sensors(
    hass: HomeAssistant,
    mock_example_client: MagicMock,
    mock_config_entry: MockConfigEntry,
    entity_registry: er.EntityRegistry,
    snapshot: SnapshotAssertion,
) -> None:
    """Test the Example sensors."""
    with patch(
        "homeassistant.components.example.PLATFORMS", [Platform.SENSOR]
    ):
        await setup_integration(hass, mock_config_entry)

    await snapshot_platform(
        hass, entity_registry, snapshot, mock_config_entry.entry_id
    )

snapshot_platform only works for one platform at a time, so an integration with more platforms limits them in the test, like above. It also fails on a disabled entity. If some of your entities are disabled by default, add the entity_registry_enabled_by_default fixture to the test, so they are enabled and end up in the snapshot.

The first time, the test fails, because there is no snapshot yet. Update the snapshots, and they end up in tests/components/example/snapshots/, in a readable .ambr file named after the test file. Commit them with your change, and read the diff: a snapshot only proves something if you checked that it is right.

Snapshots do not replace functional tests. To test that an entity becomes unavailable when the device fails, assert exactly that:

Python
assert hass.states.get("sensor.example_temperature").state == STATE_UNAVAILABLE

That says what you expect, and fails for the right reason.

What to test

  • Config flows: every step and every error, and that the flow recovers after an error and still creates the entry. Test that a second setup of the same device aborts. Reviewers often read these tests first.
  • Setup: that the integration loads and unloads, and what happens when the device is offline during setup.
  • Errors: what happens when the device goes offline later, returns garbage, or the credentials stop working. This is the test reviewers ask for most.
  • Bug fixes: a test that fails without your fix, and passes with it.
  • Avoid brittle values: do not depend on data that changes when a library is updated, like this year's holidays.

The Bronze tier of the integration quality scale requires full test coverage of the config flow, see config-flow-test-coverage. From Silver on, every module of the integration needs more than 95% coverage, see test-coverage.

The tutorial shows a complete set of tests for a small integration.

Was this page helpful?

Last updated on .