A full, reusable test container for Home Assistant.
ha-testcontainer is a Python library that wraps the official Home Assistant
Docker image in a Testcontainers-based class.
It handles all the plumbing — startup, programmatic onboarding, long-lived
token creation, and custom component mounting — so your tests can focus on
what matters.
UIX (UI eXtension) is the primary consumer and uses this library as a replacement for its legacy local test stack.
| Capability | Detail |
|---|---|
| Version flexibility | stable, beta, dev, or any pinned tag (2024.6.0) |
| Automatic onboarding | Creates an admin user and mints a long-lived API token with no manual interaction |
| Demo entities | Built-in HA demo integration gives you lights, sensors, weather objects immediately |
| Custom config | Mount any configuration.yaml tree via config_path= |
| Custom components | Mount any custom_components/ directory via custom_components_path= |
| Fetch any component | scripts/fetch_component.py owner/repo — downloads any GitHub-hosted HA component |
| Fetch any frontend plugin | scripts/fetch_plugin.py owner/repo — downloads any JS dashboard plugin; registers it as a Lovelace resource |
| Storage-mode dashboard | Default Lovelace dashboard in storage mode; REST-pushable for tests |
| REST API helper | ha.api("GET", "states") — authenticated calls with zero boilerplate |
| Integration setup | ha.setup_integration("my_domain") — drives config-flow programmatically |
| Playwright visual tests | Session-scoped browser context, token injection, pixel-diff snapshot comparison |
| Boilerplate example | examples/test_custom_component.py — copy, fill in TODO values, done |
pip install -e ".[test]"
playwright install chromium# Any GitHub-hosted HA custom component (Python, goes into custom_components/):
python scripts/fetch_component.py Lint-Free-Technology/uix
python scripts/fetch_component.py Lint-Free-Technology/uix 5.3.1 # pinned version
# Or via make (COMPONENT is required):
make setup COMPONENT=Lint-Free-Technology/uix
make setup COMPONENT=Lint-Free-Technology/uix VERSION=5.3.1Frontend plugins are JavaScript dashboard modules (Lovelace cards, etc.),
distinct from Python custom components. They are downloaded into
ha-config/www/dashboard/ and automatically served by HA at /local/….
# Download and register a dashboard plugin:
python scripts/fetch_plugin.py custom-cards/button-card
python scripts/fetch_plugin.py thomasloven/lovelace-card-mod 3.4.4
# Or via make:
make fetch-plugin PLUGIN=custom-cards/button-card
make fetch-plugin PLUGIN=thomasloven/lovelace-card-mod VERSION=3.4.4make test # unit + integration tests (no browser)
make test-visual # Playwright visual tests (requires Docker + Playwright)See Testing below for a full breakdown of each tier.
make up # starts HA at http://localhost:8123
make downfrom ha_testcontainer import HATestContainer, HAVersion
with HATestContainer(
version=HAVersion.STABLE, # or "beta", "dev", "2024.6.0"
config_path="ha-config",
custom_components_path="custom_components",
) as ha:
# Set up any custom component via config-flow
ha.setup_integration("uix")
# REST API — authenticated, zero boilerplate
states = ha.api("GET", "states").json()
print(ha.get_url()) # http://localhost:<random-port>
print(ha.get_token()) # long-lived access token# conftest.py
import pytest
from ha_testcontainer import HATestContainer
@pytest.fixture(scope="session")
def ha():
with HATestContainer(config_path="ha-config", custom_components_path="custom_components") as c:
c.setup_integration("uix")
yield c
# test_my_component.py
def test_api(ha):
resp = ha.api("GET", "states")
assert resp.status_code == 200See examples/test_custom_component.py for a
fully annotated boilerplate that any component author can copy.
from ha_testcontainer.visual import PAGE_LOAD_TIMEOUT, assert_snapshot
def test_my_card(ha_page, ha_url):
ha_page.goto(f"{ha_url}/lovelace/0", wait_until="networkidle",
timeout=PAGE_LOAD_TIMEOUT)
assert_snapshot(ha_page, "my_card_baseline")Run SNAPSHOT_UPDATE=1 pytest tests/visual/ to create or update baselines.
scripts/fetch_component.py works with any GitHub repository that follows
the standard HA custom-component layout (a custom_components/<name>/ directory
in the repository root):
python scripts/fetch_component.py owner/repo # latest release
python scripts/fetch_component.py owner/repo 5.3.1 # specific version
python scripts/fetch_component.py owner/repo --list # list releases
python scripts/fetch_component.py owner/repo --target-dir /other/path
The script auto-discovers all custom_components/ sub-directories in the
release archive, so multi-component repositories are handled correctly.
scripts/fetch_plugin.py downloads JavaScript dashboard modules — Lovelace
cards and similar frontend-only plugins — from GitHub releases. These are
distinct from Python custom components: they live in www/ rather than
custom_components/, and are loaded by the HA frontend via Lovelace resources.
python scripts/fetch_plugin.py owner/repo # latest release
python scripts/fetch_plugin.py owner/repo 1.2.3 # specific version
python scripts/fetch_plugin.py owner/repo --list # list releases
python scripts/fetch_plugin.py owner/repo --plugin-name custom-name
python scripts/fetch_plugin.py owner/repo --resource-type js # legacy (default: module)
The script:
- Downloads JS files from the release (assets, zip assets, or source archive fallback).
- Places them at
ha-config/www/dashboard/<plugin-name>/<file>.js. - Registers each file as a Lovelace resource in
ha-config/lovelace_resources.yaml.
HA serves ha-config/www/ at /local/, so the plugin is immediately available
to Lovelace dashboards as /local/dashboard/<plugin-name>/<file>.js.
| Value | Image tag pulled |
|---|---|
HAVersion.STABLE (default) |
ghcr.io/home-assistant/home-assistant:stable |
HAVersion.BETA |
ghcr.io/home-assistant/home-assistant:beta |
HAVersion.DEV |
ghcr.io/home-assistant/home-assistant:dev |
"2024.6.0" |
ghcr.io/home-assistant/home-assistant:2024.6.0 |
Override at runtime:
HA_VERSION=beta pytest tests/
HA_VERSION=2024.6.0 make test| Environment variable | Default | Purpose |
|---|---|---|
HA_VERSION |
stable |
Image tag |
HA_CONFIG_PATH |
ha-config/ |
Host dir mounted as /config |
HA_CUSTOM_COMPONENTS_PATH |
custom_components/ |
Host dir mounted as /config/custom_components |
ha-testcontainer/
├── ha_testcontainer/
│ ├── __init__.py # public API: HATestContainer, HAVersion, visual helpers
│ ├── container.py # HATestContainer implementation
│ └── visual.py # PAGE_LOAD_TIMEOUT, assert_snapshot, inject_ha_token
├── ha-config/
│ ├── configuration.yaml # demo HA config (default_config + demo integration)
│ ├── lovelace_resources.yaml # Lovelace resources list (managed by fetch_plugin.py)
│ ├── www/ # served at /local/ by HA; plugins downloaded here
│ └── themes/ # theme YAML files (auto-loaded)
├── custom_components/
│ └── README.md # populated by scripts/fetch_component.py (gitignored)
├── scripts/
│ ├── fetch_component.py # download any HA Python component from GitHub releases
│ └── fetch_plugin.py # download any JS frontend plugin; register as resource
├── examples/
│ └── test_custom_component.py # boilerplate visual test — copy & customise
├── tests/
│ ├── conftest.py # session-scoped ha / ha_url / ha_token fixtures
│ ├── test_container_unit.py # unit tests — no Docker needed (14 tests)
│ ├── test_container.py # integration tests — requires Docker (10 tests)
│ └── visual/
│ ├── conftest.py # Playwright fixtures (ha_page, ha_browser_context)
│ └── snapshots/ # gitignored — no baselines committed here (see below)
├── docker-compose.yml # local dev: docker compose up
├── Makefile # setup / test / update-snapshots targets
└── pyproject.toml
Snapshot tests follow a two-file convention:
| File | Description |
|---|---|
snapshots/<name>.png |
Committed baseline — the ground truth, lives in the consumer's repo |
snapshots/<name>.actual.png |
Generated on every run — gitignored |
On the first run (or when SNAPSHOT_UPDATE=1 is set), the actual
screenshot becomes the baseline. Subsequent runs diff the two; any pixel
difference causes the test to fail.
Baselines are placed next to the calling test file automatically — no configuration needed.
Important — baselines are stored in the consumer's repo, not here. ha-testcontainer is a reusable library; it does not commit any snapshot PNG files. All
*.pngfiles undertests/visual/snapshots/are gitignored in this repository. When you write visual tests for your own component, baselines are committed in your project'stests/visual/snapshots/directory. They will never appear in ha-testcontainer's history.
The test suite has three tiers:
These run in milliseconds and cover the Python logic of HATestContainer
in isolation (URL construction, token handling, API path normalisation):
pip install -e ".[test]" # install once
pytest tests/test_container_unit.py -vOr via make:
make install
make testmake test runs all non-browser tests, including both unit and
integration tests, skipping the visual tier.
These start a real Home Assistant container and exercise the full lifecycle (onboarding, REST API, demo entities, Lovelace push):
# Prerequisites: Docker daemon running, HA image available
pytest tests/test_container.py -vThe container is started once per pytest session (session-scoped fixture) and reused across all tests in the file. Startup takes ~60 s the first time while HA initialises.
Environment variables let you control which image and config directory are used:
| Variable | Default | Purpose |
|---|---|---|
HA_VERSION |
stable |
Image tag (stable, beta, dev, 2024.6.0, …) |
HA_CONFIG_PATH |
ha-config/ |
Host dir mounted as /config |
HA_CUSTOM_COMPONENTS_PATH |
custom_components/ |
Host dir mounted as /config/custom_components |
HA_VERSION=beta pytest tests/test_container.py -vpip install -e ".[test]"
playwright install chromium
pytest tests/visual/ -vOr via make:
make install
make test-visualVisual tests open a Chromium browser, log in to the running HA instance, and compare screenshots against committed baselines. Baselines are stored in the consumer's own repository — see Snapshot-based visual testing.
These pull the stable, beta, and dev images in sequence and verify that
the container starts for each:
pytest tests/test_container.py -v -m version_smoke
# or:
make test-smokeThey are skipped by default to avoid pulling large images on every run.
UIX's test/docker-compose.yaml + test/configuration.yaml + test/lovelace.yaml
are replaced by make up and UIX writing its own tests/ using HATestContainer.
- Add
ha-testcontaineras a dev dependency. - Fetch UIX:
python scripts/fetch_component.py Lint-Free-Technology/uix. - Replace
docker compose -f test/docker-compose.yaml upwithmake up. - Copy
examples/test_custom_component.pyinto UIX'stests/visual/, fill in UIX-specific TODO values, and add UIX-specific test cases. - Delete UIX's
test/directory.
MIT — see LICENSE.