# Writing tests

> How the tests of an integration are organized, how to test through Home Assistant instead of around it, and how to use fixtures, mocks, and snapshots.

Source: https://contributors.home-assistant.io/core/development/tests/

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](https://contributors.home-assistant.io/core/development/testing/).

## 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:

```text
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.

- Set up the integration with `hass.config_entries.async_setup()`, or with `async_setup_component()` if it has no config entries.
- Read the state of entities from the state machine, with `hass.states.get()`.
- Call actions through `hass.services.async_call()`.
- Check devices and entities in the device and entity registries.
- Check the state of a config entry with its `state` attribute.
- Create config entries with `MockConfigEntry` from [`tests/common.py`](https://github.com/home-assistant/core/blob/dev/tests/common.py).

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`](https://github.com/home-assistant/core/blob/dev/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`](https://github.com/home-assistant/core/blob/dev/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](https://github.com/syrupy-project/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](https://contributors.home-assistant.io/core/development/testing/#update-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](https://contributors.home-assistant.io/core/quality-scale/) requires full test coverage of the config flow, see [config-flow-test-coverage](https://contributors.home-assistant.io/core/quality-scale/config-flow-test-coverage/). From Silver on, every module of the integration needs more than 95% coverage, see [test-coverage](https://contributors.home-assistant.io/core/quality-scale/test-coverage/).

The [tutorial](https://contributors.home-assistant.io/core/integrations/tutorial/#9-write-the-tests) shows a complete set of tests for a small integration.
