Factories & Internals
Event Factories
hassette.test_utils exports six factory functions for building raw event and state dictionaries. These are useful when you need to construct events manually — for example, to pre-populate state before a test or to pass custom event data to lower-level bus methods.
from hassette.test_utils import (
create_call_service_event,
create_state_change_event,
make_light_state_dict,
make_sensor_state_dict,
make_state_dict,
make_switch_state_dict,
)
create_state_change_event
Creates a state_changed event object suitable for sending through the bus directly.
from hassette.test_utils import create_state_change_event
event = create_state_change_event(
entity_id="binary_sensor.motion",
old_value="off",
new_value="on",
old_attrs={"device_class": "motion"},
new_attrs={"device_class": "motion"},
)
All parameters except entity_id, old_value, and new_value are optional.
create_call_service_event
Creates a call_service event object.
from hassette.test_utils import create_call_service_event
event = create_call_service_event(
domain="light",
service="turn_on",
service_data={"entity_id": "light.kitchen", "brightness": 200},
)
State Factories
make_state_dict
Creates a raw state dictionary in Home Assistant format. The harness uses this internally; you'll use it when constructing test data directly.
from hassette.test_utils import make_state_dict
state = make_state_dict(
"sensor.temperature",
"21.5",
attributes={"unit_of_measurement": "°C", "device_class": "temperature"},
)
All parameters except entity_id and state are optional. Timestamps default to now.
make_light_state_dict
Shorthand for light entity state dicts with common attributes.
from hassette.test_utils import make_light_state_dict
state = make_light_state_dict(
entity_id="light.kitchen",
state="on",
brightness=200,
color_temp=370,
)
| Parameter | Default | Description |
|---|---|---|
entity_id |
"light.kitchen" |
Light entity ID. |
state |
"on" |
"on" or "off". |
brightness |
None |
Brightness 0–255. Omitted if not set. |
color_temp |
None |
Color temperature in mireds. Omitted if not set. |
**kwargs |
— | Extra attributes or state dict fields (last_changed, last_updated, context). |
make_sensor_state_dict
Shorthand for sensor entity state dicts.
from hassette.test_utils import make_sensor_state_dict
state = make_sensor_state_dict(
entity_id="sensor.temperature",
state="21.5",
unit_of_measurement="°C",
device_class="temperature",
)
| Parameter | Default | Description |
|---|---|---|
entity_id |
"sensor.temperature" |
Sensor entity ID. |
state |
"25.5" |
Sensor value as a string. |
unit_of_measurement |
None |
Unit string, e.g. "°C", "%". |
device_class |
None |
HA device class, e.g. "temperature". |
make_switch_state_dict
Shorthand for switch entity state dicts.
from hassette.test_utils import make_switch_state_dict
state = make_switch_state_dict(entity_id="switch.outlet", state="off")
make_mock_hassette
make_mock_hassette() builds a sealed AsyncMock hassette with real, Pydantic-validated configuration. It is the standard pattern for unit tests that need a hassette mock with validated config — it replaces the pattern of manually setting .config.* fields on a raw AsyncMock.
from pathlib import Path
from hassette.test_utils import make_mock_hassette
tmp_path = Path("/tmp/test") # pyright: ignore[reportUnusedVariable]
# Minimal — real config defaults, sealed against phantom attributes
hassette = make_mock_hassette()
# With config overrides — validated by HassetteConfig at construction time
hassette = make_mock_hassette(strict_lifecycle=True)
hassette = make_mock_hassette(database={"retention_days": 14})
# Database-backed tests — pass a real tmp_path for isolation
hassette = make_mock_hassette(data_dir=tmp_path)
All HassetteConfig fields can be passed as keyword arguments; Pydantic validates them at construction time. Passing an unrecognised field name or an out-of-range value raises pydantic.ValidationError immediately.
Non-config attributes (ready_event, shutdown_event, session_id, _scheduler_service, _bus_service, wait_for_ready, children, etc.) are wired automatically. By default the mock is seal()-ed — accessing an attribute not set by the factory raises AttributeError. Pass sealed=False to add extra attributes after construction.
| Parameter | Default | Description |
|---|---|---|
data_dir |
tempfile.mkdtemp() |
Directory for Hassette data files. Pass tmp_path in integration tests for isolation. |
set_ready |
True |
Pre-set ready_event so wait_for_ready() resolves immediately. |
set_loop |
True |
Wire loop to asyncio.get_running_loop(). Pass False for session-scoped fixtures that run outside an event loop. |
sealed |
True |
Call seal() after wiring; accessing unlisted attributes raises AttributeError. |
**config_overrides |
— | Any HassetteConfig field. Merged on top of test-appropriate defaults. |
make_test_config
AppTestHarness creates a minimal HassetteConfig internally. If you need a HassetteConfig without the full harness — for example, to test configuration parsing logic directly — use make_test_config:
from pathlib import Path
from hassette.test_utils import make_test_config
def test_config_defaults(tmp_path: Path):
config = make_test_config(data_dir=tmp_path)
assert config.web_api.run is False
# Override specific fields
config = make_test_config(data_dir=tmp_path, base_url="http://192.168.1.100:8123")
assert config.base_url == "http://192.168.1.100:8123"
def test_config_overrides(tmp_path: Path):
config = make_test_config(data_dir=tmp_path, token="my-real-token", web_api={"run": True})
assert config.token == "my-real-token"
assert config.web_api.run is True
make_test_config reads nothing from TOML files, env vars, or the CLI — only the values you pass are used. Pydantic validation still runs.
data_dir is required — pass a tmp_path fixture value in pytest. All other fields have test-appropriate defaults:
| Field | Default |
|---|---|
data_dir |
required — no default |
token |
"test-token" |
base_url |
"http://test.invalid:8123" |
disable_state_proxy_polling |
True |
app |
{"autodetect": False} |
web_api |
{"run": False} |
run_app_precheck |
False |
Pass **overrides to replace any of the defaults.
RecordingApi Coverage Boundary
RecordingApi stubs write methods and delegates state reads to the seeded StateProxy. Anything that requires a live HA connection raises NotImplementedError.
Explicit stubs (raise NotImplementedError directly):
get_state_raw()get_states_raw()get_history()render_template()ws_send_and_wait()ws_send_json()rest_request()delete_entity()
Redirected via __getattr__ (raise NotImplementedError with a message pointing to get_state()):
get_state_value()get_state_value_typed()get_attribute()
Any other public method not explicitly defined on RecordingApi also falls through to __getattr__ and raises NotImplementedError.
For all of the above, seed the data you need via harness.set_state() and use the read methods that delegate to StateProxy: get_state(), get_states(), get_entity(), get_entity_or_none(), entity_exists(), get_state_or_none().
api.sync is a recording facade
harness.api_recorder.sync is a _RecordingSyncFacade — a recording proxy, not a Mock. Write calls made via self.api.sync.* appear in the same api_recorder.calls list as their async counterparts and can be asserted with the same API:
from hassette.test_utils import AppTestHarness
from my_apps.sync_app import SyncApp
async def test_sync_facade_recording():
async with AppTestHarness(SyncApp, config={}) as harness:
await harness.simulate_state_change("binary_sensor.motion", old_value="off", new_value="on")
# Your app calls: self.api.sync.turn_on("light.kitchen", domain="light")
harness.api_recorder.assert_called("turn_on", entity_id="light.kitchen", domain="light")
Methods not covered by the facade raise NotImplementedError rather than silently succeeding.
Note
The list of NotImplementedError methods above reflects RecordingApi at the time this page was written. If you encounter an unexpected NotImplementedError, check src/hassette/test_utils/recording_api.py for the current state.
Tier 2 Re-exports
hassette.test_utils also re-exports a set of Tier 2 symbols — internal utilities used by Hassette's own test suite — for backward compatibility. These are not in __all__ and may change without notice. They are not documented here. Use Tier 1 APIs (AppTestHarness, RecordingApi, the factory functions, make_test_config, and the drain exception types) for all end-user testing.
Next Steps
- Quick Start: Back to the harness basics
- Time Control: Freeze and advance time for scheduler tests
- Concurrency & pytest-xdist: Understand the concurrency locks