ha-testcontainer
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.
Features
| Capability | Detail |
|---|---|
| Version flexibility | stable, beta 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 |
Quick start
1 — Install
pip install -e ".[test]"
playwright install chromium
2 — Fetch a custom component
# 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.1
2b — Fetch a frontend plugin (optional)
Frontend 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.4
3 — Run tests
make 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.
4 — Explore locally with docker compose
make up # starts HA at http://localhost:8123
make down
Usage in your own project
from 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
pytest plugin (zero-boilerplate visual testing)
When you install ha-testcontainer[test], the package registers a pytest
plugin automatically via the pytest11 entry-point. The plugin provides the
following fixtures out-of-the-box — no conftest.py import required:
| Fixture | Scope | Description |
|---|---|---|
ha |
session | Running HA instance (Docker or external) |
ha_url |
session | Base URL, e.g. http://localhost:8123 |
ha_token |
session | Long-lived access token |
ha_lovelace_url_path |
session | URL path of the test Lovelace dashboard |
ha_browser_context |
session | Pre-authenticated Playwright browser context |
ha_page |
function | Fresh Playwright page (auth inherited from context) |
Configure the plugin with environment variables:
| Variable | Default | Purpose |
|---|---|---|
HA_VERSION |
stable |
Docker image tag |
HA_CONFIG_PATH |
(none) | Host config dir mounted as /config |
HA_CUSTOM_COMPONENTS_PATH |
(none) | Host dir mounted as /config/custom_components |
HA_SETUP_INTEGRATION |
(none) | Integration domain to configure via config-flow |
HA_INTEGRATIONS_YAML |
(none) | Path to an integrations.yaml file; each entry downloads a custom integration and runs config-flow |
HA_EXTRA_CONFIG_DIR |
(none) | Directory merged on top of HA_CONFIG_PATH |
HA_PLUGINS_YAML |
(none) | Path to a plugins.yaml listing Lovelace plugins |
HA_URL + HA_TOKEN |
(none) | Connect to a pre-running HA instance instead of Docker |
pytest fixture example (manual, without plugin)
# 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 == 200
Playwright visual test example
See 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.
YAML-driven scenario tests
The scenario runner (ha_testcontainer.visual.scenario_runner) lets you write
visual tests as plain YAML files — no Python code required. Configure it in
your conftest.py and it automatically picks up all *.yaml files in your
scenarios directory.
See also: Writing Scenarios guide — best practices for setting entity states, structuring test cards, and keeping the test environment isolated from any locally-running Home Assistant instance.
See also: Generating Documentation Images guide — how to use
doc_image:anddoc_animation:to capture and verify documentation screenshots and GIFs.
# tests/conftest.py
from pathlib import Path
import ha_testcontainer.visual.scenario_runner as sr
sr.SCENARIOS_DIR = Path(__file__).parent / "scenarios"
sr.SNAPSHOTS_DIR = Path(__file__).parent / "snapshots"
sr.REPO_ROOT = Path(__file__).parent.parent
# tests/visual/test_scenarios.py
import pytest
from playwright.sync_api import Page
from ha_testcontainer.visual.scenario_runner import (
load_all_scenarios, push_scenario, goto_scenario,
run_interactions, run_assertions, clear_scenario,
)
_ALL = load_all_scenarios()
@pytest.mark.parametrize("scenario_id", [s["id"] for s in _ALL])
def test_scenario(scenario_id, ha, ha_page: Page, ha_url, ha_lovelace_url_path):
scenario = next(s for s in _ALL if s["id"] == scenario_id)
push_scenario(ha, ha_lovelace_url_path, scenario)
try:
goto_scenario(ha_page, ha_url, ha_lovelace_url_path, scenario["view_path"])
run_interactions(ha_page, scenario, ha=ha)
run_assertions(ha_page, scenario)
finally:
clear_scenario(ha, ha_lovelace_url_path)
Fetching custom components
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.
Fetching frontend plugins (dashboard cards)
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.
Configuration
HA version
| 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
Config & custom components
| 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_SETUP_INTEGRATION |
(unset) | Integration domain to configure via config-flow |
HA_INTEGRATIONS_YAML |
(unset) | Path to an integrations.yaml file; each entry downloads a custom integration and runs config-flow |
HA_EXTRA_CONFIG_DIR |
(unset) | Directory merged on top of HA_CONFIG_PATH before start |
HA_PLUGINS_YAML |
(unset) | Path to a plugins.yaml listing Lovelace plugins to download |
HA_INTEGRATIONS_YAML file format:
- repo: Lint-Free-Technology/uix
domain: uix
version: 5.3.1 # optional; latest release is used when omitted
Repository layout
ha-testcontainer/
├── ha_testcontainer/
│ ├── __init__.py # public API: HATestContainer, HAVersion, visual helpers
│ ├── container.py # HATestContainer implementation
│ ├── plugins.py # download_lovelace_plugins — fetch Lovelace JS plugins
│ ├── ha_server.py # persistent HA dev server (python -m ha_testcontainer.ha_server)
│ ├── pytest_plugin.py # pytest plugin: ha, ha_url, ha_token, ha_page fixtures
│ └── visual/
│ ├── __init__.py # PAGE_LOAD_TIMEOUT, assert_snapshot, inject_ha_token
│ ├── cursors.py # SVG cursor overlays for doc images/animations
│ ├── lovelace_helpers.py # push_lovelace_config_to WS helper
│ └── scenario_runner.py # YAML-driven scenario engine
├── 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
├── ha-tests/ # reference integration / thin shims (authoritative code is in ha_testcontainer/)
│ ├── conftest.py # configures ha_testcontainer plugin paths for this test suite
│ ├── ha_server.py # shim → ha_testcontainer.ha_server
│ ├── plugins.py # shim → ha_testcontainer.plugins
│ ├── plugins.yaml # Lovelace plugin registry for ha-tests/
│ ├── integrations.yaml # integration registry for HA_INTEGRATIONS_YAML
│ ├── ha-config/ # HA config for the ha-tests/ suite
│ └── visual/
│ ├── conftest.py # shim (fixtures come from the pytest plugin)
│ ├── scenario_runner.py # shim → ha_testcontainer.visual.scenario_runner
│ ├── lovelace_helpers.py # shim → ha_testcontainer.visual.lovelace_helpers
│ ├── cursors.py # shim → ha_testcontainer.visual.cursors
│ ├── scenarios/ # YAML scenario files for this test suite
│ └── snapshots/ # gitignored baseline PNGs
├── 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-based visual testing
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.
Testing
The test suite has three tiers:
Tier 1 — Unit tests (no Docker required)
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 -v
Or via make:
make install
make test
make test runs all non-browser tests, including both unit and
integration tests, skipping the visual tier.
Tier 2 — Integration tests (Docker required)
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 -v
The 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 -v
Tier 3 — Visual (Playwright) tests (Docker + Playwright required)
pip install -e ".[test]"
playwright install chromium
pytest tests/visual/ -v
Or via make:
make install
make test-visual
Visual 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.
Version smoke tests (slow, optional)
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-smoke
They are skipped by default to avoid pulling large images on every run.
License
MIT — see LICENSE.
Metadata
Release files for ha-testcontainer 2.9.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| ha_testcontainer-2.9.0.tar.gz | 70.7 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ha_testcontainer-2.9.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 131.9 kB
Release files / ha_testcontainer-2.9.0.tar.gz
| Download URL | ha_testcontainer-2.9.0.tar.gz |
|---|---|
| Size | 70.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0be2611926be2636192acf16a3429264f256c08d0df18478c4a86811ab529dea
|
|
BLAKE2b-256 checksum How to use checksums |
55063d2cc76c9883d0c06a1491e51c8f3e300eb534b1ad4bdb6a867b8a501af0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.
Transparency logRelease files / ha_testcontainer-2.9.0-py3-none-any.whl
| Download URL | ha_testcontainer-2.9.0-py3-none-any.whl |
|---|---|
| Size | 61.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fc53597ccad2faf2cddaaec66e2c3ef3da10b9c44504c6eac0c0baa00f4dc831
|
|
BLAKE2b-256 checksum How to use checksums |
f370bc50f45ce5c5ec5cf3299faabf1fe2bbb34ba8e65ab4d5a8a38c9ea8dd5e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.
Transparency logSecurity for ha-testcontainer 2.9.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for ha_testcontainer-2.9.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.