Production-ready Test-Suite mit 99% Code Coverage und 108 Tests.
- 108 Tests PASSED (100% Success Rate)
- 99% Code Coverage (426 von 427 Zeilen getestet)
- Python 3.12 kompatibel (getestet)
- Python 3.11 kompatibel (Home Assistant Minimum-Version)
- Production-Ready
| Modul | Statements | Missing | Coverage |
|---|---|---|---|
__init__.py |
27 | 0 | 100% |
config_flow.py |
78 | 0 | 100% |
const.py |
6 | 0 | 100% |
coordinator.py |
146 | 0 | 100% |
sensor.py |
169 | 1 | 99% |
| TOTAL | 426 | 1 | 99% |
Die eine fehlende Zeile (sensor.py:154) ist ein unerreichbarer Code-Pfad (defensive Warnung, die nie ausgelöst werden kann).
tests/
├── __init__.py # Test-Paket
├── conftest.py # Shared fixtures und Test-Konstanten (281 Zeilen)
├── test_coordinator.py # API & Coordinator Tests (39 Tests)
├── test_config_flow.py # Config & Options Flow Tests (19 Tests)
├── test_init.py # Integration Setup/Unload Tests (20 Tests)
├── test_sensor.py # Sensor Entity Tests (30 Tests)
└── README.md # Diese Datei
cd ~/repos/marstek_cloud
python3 -m venv venv
source venv/bin/activate
pip install -r requirements_test.txt# Alle Tests ausführen
pytest
# Mit Coverage-Report
pytest --cov=custom_components.marstek_cloud --cov-report=term-missing
# Spezifische Test-Datei
pytest tests/test_coordinator.py -v
# HTML Coverage Report
pytest --cov=custom_components.marstek_cloud --cov-report=html
open htmlcov/index.html# Python 3.12 (primär)
source venv/bin/activate
pytest
# Python 3.11 (Home Assistant minimum)
python3.11 -m venv venv311
source venv311/bin/activate
pip install -r requirements_test.txt
pytestAlle Tests folgen strikten Qualitätsstandards.
IMMER Fixtures aus conftest.py verwenden, NIEMALS MockConfigEntry duplizieren.
# GOOD
async def test_something(hass, mock_entry, init_integration):
coordinator = hass.data[DOMAIN][mock_entry.entry_id]
# BAD - Duplicated setup
async def test_something(hass):
entry = MockConfigEntry(domain=DOMAIN, data={...})IMMER Konstanten aus conftest.py verwenden.
# GOOD
from .conftest import TEST_HOST, TEST_SCAN_INTERVAL
assert config["host"] == TEST_HOST
# BAD - Magic values
assert config["host"] == "192.168.1.100"Bei 3+ ähnlichen Tests → @pytest.mark.parametrize
@pytest.mark.parametrize(
("status_code", "expected_error"),
[(401, "invalid_auth"), (503, "temporarily unavailable")],
)
async def test_http_errors(status_code, expected_error):
...JEDE Assertion MUSS eine Beschreibung haben.
# GOOD
assert len(devices) == 2, "Should return 2 devices from API"
# BAD
assert len(devices) == 2Alle Coordinators MÜSSEN async_shutdown() aufrufen.
async def test_coordinator():
coordinator = MarstekCoordinator(hass, api, interval)
try:
await coordinator.async_refresh()
# test logic
finally:
await coordinator.async_shutdown() # PREVENTS TIMER ERRORSTestMarstekAPI (31 Tests):
- Token-Retrieval: Success, 401, 500+, unexpected status, timeout, network error
- JSON-Handling: Invalid JSON, non-dict response, missing fields
- Device-Retrieval: Success, auto token fetch, token expiration refresh
- Error-Codes: -1, 401, 403 (mit Token-Refresh), Code 8 (no permission)
- Device-Filterung: IGNORED_DEVICE_TYPES
- Retry-Logik: Failed retry, invalid JSON on retry, network error on retry
- Edge-Cases: Missing data field, data not list, server errors
TestMarstekCoordinator (8 Tests):
- Initialisierung und erste Refresh
- Latency-Messung bei jedem Refresh
- Error-Handling: UpdateFailed, Network Errors
- Multiple Refresh-Cycles
- Empty Device-List
- Shutdown Cleanup
TestValidateInput (3 Tests):
- Erfolgreiche Validierung
- Invalid Auth (InvalidAuth Exception)
- Cannot Connect (CannotConnect Exception)
TestMarstekConfigFlow (16 Tests):
- User Flow: Success, Invalid Auth, Cannot Connect, Unknown Error
- Reauth Flow: Success, Invalid Auth, Cannot Connect, Unknown Error
- Options Flow: Success, No Devices
- Scan Interval Validation: 10-3600s valid, 5/5000 invalid (parametrized)
TestIntegrationSetup (20 Tests):
- Erfolgreicher Setup
- Auth Failed → ConfigEntryAuthFailed
- Connection Error → ConfigEntryNotReady
- API Error → ConfigEntryNotReady
- Erfolgreicher Unload
- Reload
- Config Entry Update mit Devices
- Coordinator Storage in hass.data
- Scan Interval aus Options vs. Data
- Multiple Entries
- Options Update triggert Reload
TestSensorSetup (4 Tests):
- Entities werden erstellt (alle Sensor-Typen)
- No Devices → Keine Entities
- Invalid devid → Devices überspringen
- Device Info korrekt
TestMarstekSensor (8 Tests):
- SOC, Charge, Discharge Power Sensoren
- Report Time: Unix Timestamp, ISO String, Invalid Formats (parametrized)
- Missing Device → None
- Units und Device Classes (parametrized: 5 Sensor-Typen)
TestMarstekDiagnosticSensor (4 Tests):
- Last Update (Timestamp, success/failed)
- API Latency
- Connection Status (online/offline)
- Unknown Key → None
TestMarstekTotalChargeSensor (3 Tests):
- Berechnung über alle Devices
- Attributes (device_count)
- No Devices → 0
TestMarstekTotalPowerSensor (2 Tests):
- Berechnung über alle Devices
- Attributes (device_count)
TestMarstekDeviceTotalChargeSensor (3 Tests):
- Device-spezifische Berechnung
- Attributes (device_name, capacity)
- Missing Device → None
TestMarstekCalculatedChargePowerSensor (3 Tests):
- Positive Charge Power (PV > discharge)
- Zero wenn discharging
- Missing Device → 0
TestMarstekCalculatedDischargePowerSensor (3 Tests):
- Positive Discharge Power (discharge > PV)
- Zero wenn charging
- Missing Device → 0
Alle Magic-Values als Konstanten:
TEST_EMAIL = "test@example.com"
TEST_PASSWORD = "test_password"
TEST_TOKEN = "test_token_12345"
TEST_SCAN_INTERVAL = 60
TEST_CAPACITY_KWH = 5.12
TEST_DEVICE_1 = {...} # Realistische Device-Daten (charging)
TEST_DEVICE_2 = {...} # Realistische Device-Daten (discharging)
TEST_DEVICE_MINIMAL = {...} # Edge-Case: Minimale Felder
TEST_DEVICE_IGNORED = {...} # Device mit ignored type (HME-3)mock_config_entry: Standard Config Entry mit 2 Devicesmock_config_entry_no_devices: Config Entry ohne Devices (Edge-Case)
mock_marstek_api: Vollständig konfigurierte API mit Standard-Responsesmock_marstek_api_single_device: API mit nur 1 Devicemock_marstek_api_no_devices: API ohne Devicesmock_aiohttp_session: Mock für aiohttp ClientSession
setup_integration: Vollständig initialisierte Integration mit Mockssetup_integration_no_devices: Integration ohne Devices (Edge-Case)
Factory-Pattern für flexible API-Response-Erstellung:
mock_api_response_success(devices=None)
mock_api_response_error(code, message)
mock_api_response_token_expired()
mock_api_response_auth_failed()
mock_api_response_no_permission()Problem: Ein Test (test_validate_input_success) verursacht einen Thread-Leak-Error.
Ursache: Home Assistant's Safe Shutdown Loop startet einen Background-Thread.
Status: Bekanntes Home Assistant Framework-Issue, kein Fehler in unserem Code.
Impact: Test selbst läuft erfolgreich (PASSED), nur Teardown-Warning.
Code:
if entities:
_LOGGER.info(f"Adding {len(entities)} ...")
async_add_entities(entities)
else:
_LOGGER.warning("No entities created") # Line 154 - unreachableUrsache: Globale Sensoren werden IMMER hinzugefügt, entities ist nie leer.
Status: Defensive Programmierung, kann nie getriggert werden.
Impact: 99% Coverage statt 100%, funktional korrekt.
Die Tests sind CI-ready für GitHub Actions:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.11', '3.12']
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install -r requirements_test.txt
- name: Run tests with coverage
run: |
pytest --cov=custom_components.marstek_cloud --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xmlpytest -v -s --log-cli-level=DEBUGpytest tests/test_coordinator.py::TestMarstekAPI::test_get_token_success --pdbpytest tests/test_coordinator.py --cov=custom_components.marstek_cloud.coordinator --cov-report=term-missingpytest --lf # Last Failed
pytest --failed-first- Home Assistant Testing Docs
- pytest-homeassistant-custom-component
- Home Assistant Core Tests
- AI Agent Guidelines
WAS WIR MOCKEN:
- API-Aufrufe (aiohttp requests)
- Externe Services
- Zeit-Operationen (für deterministische Tests)
WAS WIR NICHT MOCKEN:
- Home Assistant Core
- DataUpdateCoordinator
- Entities
- Unsere eigene Integration-Logik
Alle Tests, die einen Coordinator verwenden, MÜSSEN async_shutdown() aufrufen:
async def test_something():
coordinator = MarstekCoordinator(hass, api, interval)
try:
await coordinator.async_refresh()
# Test-Logik
finally:
await coordinator.async_shutdown() # MANDATORY!Alle Magic-Values sind als Konstanten definiert:
TEST_EMAIL,TEST_PASSWORD,TEST_TOKENTEST_SCAN_INTERVAL,TEST_CAPACITY_KWHTEST_DEVICE_1,TEST_DEVICE_2- Realistische Device-DatenTEST_DEVICE_MINIMAL- Edge-Case: Minimale FelderTEST_DEVICE_IGNORED- Device mit ignored type (HME-3)
mock_config_entry: Standard Config Entry mit 2 Devicesmock_config_entry_no_devices: Config Entry ohne Devices (Edge-Case)
mock_marstek_api: Vollständig konfigurierte API mit Standard-Responsesmock_marstek_api_single_device: API mit nur 1 Devicemock_marstek_api_no_devices: API ohne Devicesmock_aiohttp_session: Mock für aiohttp ClientSession
setup_integration: Vollständig initialisierte Integration mit Mockssetup_integration_no_devices: Integration ohne Devices (Edge-Case)
Factory-Pattern für flexible API-Response-Erstellung:
mock_api_response_success(devices=None)mock_api_response_error(code, message)mock_api_response_token_expired()mock_api_response_auth_failed()mock_api_response_no_permission()
TestMarstekAPI:
- ✅ Token-Retrieval (Success, 401, 500+, Timeout, Network Error)
- ✅ Invalid JSON und fehlende Token-Felder
- ✅ Device-Retrieval (Success, Token-Refresh bei Expiration)
- ✅ Error-Codes (-1, 401, 403, 8)
- ✅ Device-Filterung (IGNORED_DEVICE_TYPES)
- ✅ Fehlende data-Felder und Timeouts
TestMarstekCoordinator:
- ✅ Initialisierung und erste Refresh
- ✅ Latency-Messung bei jedem Refresh
- ✅ Error-Handling (UpdateFailed, Network Errors)
- ✅ Multiple Refresh-Cycles
- ✅ Empty Device-List
- ✅ Shutdown Cleanup
TestValidateInput:
- ✅ Erfolgreiche Validierung
- ✅ Invalid Auth (InvalidAuth Exception)
- ✅ Cannot Connect (CannotConnect Exception)
TestMarstekConfigFlow:
- ✅ User Flow (Success, Invalid Auth, Cannot Connect, Unknown Error)
- ✅ Reauth Flow (Success, Invalid Auth)
- ✅ Options Flow (Success, No Devices)
- ✅ Scan Interval Validation (parametrized: 10-3600s)
TestIntegrationSetup:
- ✅ Erfolgreicher Setup
- ✅ Auth Failed → ConfigEntryAuthFailed
- ✅ Connection Error → ConfigEntryNotReady
- ✅ API Error → ConfigEntryNotReady
- ✅ Erfolgreicher Unload
- ✅ Reload
- ✅ Config Entry Update mit Devices
- ✅ Coordinator Storage in hass.data
- ✅ Scan Interval aus Options vs. Data
- ✅ Multiple Entries
TestSensorSetup:
- ✅ Entities werden erstellt
- ✅ No Devices → Keine Entities
- ✅ Device Info korrekt
TestMarstekSensor:
- ✅ SOC, Charge, Discharge, PV, Grid, Load Sensoren
- ✅ Report Time (Unix Timestamp, Invalid)
- ✅ Missing Device → None
- ✅ Units und Device Classes (parametrized)
TestMarstekDiagnosticSensor:
- ✅ Last Update (Timestamp)
- ✅ API Latency
- ✅ Connection Status (online/offline)
TestMarstekTotalChargeSensor:
- ✅ Berechnung über alle Devices
- ✅ Attributes (device_count)
- ✅ No Devices → 0
TestMarstekTotalPowerSensor:
- ✅ Berechnung über alle Devices
- ✅ Attributes (device_count)
TestMarstekDeviceTotalChargeSensor:
- ✅ Device-spezifische Berechnung
- ✅ Attributes (device_name, capacity)
TestMarstekCalculatedChargePowerSensor:
- ✅ Positive Charge Power (PV > discharge)
- ✅ Zero wenn discharging
- ✅ Attributes (calculation_method, raw values)
TestMarstekCalculatedDischargePowerSensor:
- ✅ Positive Discharge Power (discharge > PV)
- ✅ Zero wenn charging
- ✅ Attributes (calculation_method, raw values)
IMMER Fixtures aus conftest.py verwenden, NIEMALS MockConfigEntry etc. duplizieren.
IMMER Konstanten aus conftest.py verwenden (TEST_EMAIL, TEST_SCAN_INTERVAL, etc.).
Bei 3+ ähnlichen Tests → @pytest.mark.parametrize verwenden.
JEDE Assertion MUSS eine Beschreibung haben (außer pytest.raises).
# GOOD
assert len(devices) == 2, "Should return 2 devices from API"
# BAD
assert len(devices) == 2Wiederholte Patterns → Helper-Fixtures in conftest.py mit Factory-Pattern.
Kein Copy-Paste von Setup-Code → Fixtures verwenden.
@pytest.mark.parametrize(
("exception", "expected_error"),
[
(asyncio.TimeoutError(), "timeout"),
(aiohttp.ClientError(), "connection"),
(aiohttp.ClientResponseError(None, None, status=401), "auth"),
],
)
async def test_api_errors(exception, expected_error):
mock_api.get_devices.side_effect = exception
with pytest.raises(UpdateFailed):
await coordinator.async_refresh()# GOOD - Check Kern-Inhalt
error_msg = str(exc_info.value).lower()
assert "timeout" in error_msg or "api" in error_msg
# BAD - Zu spezifisch, bricht bei Umformulierungen
assert str(exc_info.value) == "API request timeout - check network connection"Nach Test-Ausführung:
# Terminal-Report
pytest --cov=custom_components.marstek_cloud --cov-report=term-missing
# HTML-Report (detailliert)
pytest --cov=custom_components.marstek_cloud --cov-report=html
open htmlcov/index.htmlDie Tests sind CI-ready und können in GitHub Actions integriert werden:
- name: Run tests with coverage
run: |
pytest --cov=custom_components.marstek_cloud --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3pytest -v -s --log-cli-level=DEBUGpytest tests/test_coordinator.py::TestMarstekAPI::test_get_token_success --pdbpytest tests/test_coordinator.py --cov=custom_components.marstek_cloud.coordinator --cov-report=term-missing- WSL-Timer Issues: Coordinator MUSS
async_shutdown()in finally blocks aufrufen - Async Tests: Alle Tests die mit HA interagieren MÜSSEN
async defsein - Timezone: Report-Time Tests verwenden
dt_utilfür konsistente Timezones