01 — Overview¶
What this suite tests¶
eufy_vacuum is a Home Assistant custom integration built on an adapter
pattern: brand-specific entity wiring lives in adapters/, and a large
brand-agnostic core (the manager, the job pipeline, learning, mapping,
battery, rooms, themes, setup) sits on top. The test suite targets that core
plus the HA integration seams — config flow, services, platforms, listeners.
The goal is regression safety for refactors. The core is being split into
subsystem packages over time (see docs/dev/), and the suite exists so those
moves can happen without silently breaking behavior.
The three layers¶
| Layer | Directory | Needs hass? |
What it covers |
|---|---|---|---|
| Unit | tests/unit/ |
No (mostly) | Pure functions and isolated class methods — timestamp parsing, battery metrics math, learning estimator/finalizer helpers, room-field resolution. Fast, no I/O. |
| Integration | tests/integration/ |
Yes | Anything that touches the manager, the HA service registry, platforms, or the persistent store. Uses the in-memory hass from pytest-homeassistant-custom-component. |
| Adapter | tests/adapters/ |
No | Brand-specific pure logic, now spanning two brands — Eufy (model catalog, discovery, segmentor, room attribution) and Roborock (roborock/test_adapter.py) — plus the brand-agnostic conformance harness. Kept on its own path; counted in the coverage number — we always test the adapters we ship. |
Rule of thumb: if the thing under test is a pure function, write a unit test — it is faster, has no shared-state pitfalls, and reads more clearly. Reach for an integration test only when you need the manager, a real service call, or the store.
Directory layout¶
tests/
conftest.py # root fixtures: hass enablement, config entries
__init__.py
unit/ # pure-function tests
test_timestamp_utils.py
test_battery_metrics.py
test_learning_estimator.py
test_learning_job_finalizer.py
...
integration/
conftest.py # manager + manager_with_services + seeding helpers
test_config_flow.py
test_manager_setup.py
test_services_rooms.py
test_learning_services.py
test_button_entity.py
test_listeners_*.py
test_setup_*.py
test_themes_*.py
...
adapters/
conftest.py # adapter-suite fixtures
test_adapter_contract.py # brand-agnostic conformance harness
eufy/ # concrete-adapter tests (counted in coverage %)
test_model_catalog.py
test_discovery.py
test_lifecycle.py
test_buttons_entities.py
test_segmentor.py
test_segmentor_splitters.py
test_job_segmenter_config.py
test_room_attribution.py
roborock/ # second concrete adapter (S6-class)
test_adapter.py
Toolchain¶
Declared in requirements_test.txt:
| Package | Why |
|---|---|
pytest>=9.0 |
runner |
pytest-asyncio>=1.3 |
async tests; asyncio_mode = auto means async def test_* just works, no decorator |
pytest-cov |
coverage + branch coverage |
pytest-homeassistant-custom-component>=0.13.332 |
the in-memory HA harness — provides the hass and enable_custom_integrations fixtures |
Config lives in two files:
pytest.ini—asyncio_mode = auto,testpaths = tests/unit tests/integration, and the coverageaddopts(term-missing + branch). A default run does not write HTML; the canonicalhtmlcov/is produced only by an explicit--cov-report=html:htmlcov..coveragerc— coveragesourceiscustom_components/eufy_vacuum(noomit— the eufy adapter is counted too);exclude_linesdropsTYPE_CHECKING,__repr__,NotImplementedError,pragma: no cover, bare...stub bodies (^\s*\.\.\.\s*$), and@overload-decorated signatures (@(typing\.)?overload).
Note that testpaths deliberately excludes tests/adapters — run that path
explicitly when you want it (see 02).
Coverage status¶
Coverage: 95.2% statement (93% combined with --cov-branch, which the
default addopts enables), across the brand-agnostic core and the Eufy
adapter — the adapter is now counted in the number (see
subsystems/15-adapters). Most subsystems sit in the
92–98% band; the most visible thin spot is the CV segmentor (91%). The
per-subsystem breakdown lives in
subsystems/README.
Coverage is a guide, not a target — the suite favors precision (each test maps to a named behavior, see 04) over chasing the last few percent. The remaining misses are, by design, one of:
# pragma: no cover— pure log-only / best-effortexceptblocks (I/O writes, listener teardown). Never used where the failure path escapes into a returned/persisted/user-visible value.- Honest misses (not tested, not pragma'd) — defensive
continue/return []guards (real control flow) andasync_setup_entryboot wiring that only runs under a full integration boot.
See subsystems/README for the full convention. 95% is not pursued: reaching it on this codebase would mean either pragma-bombing real control flow or writing tests that exist only to move a number.