| name | bsblan-testing |
| description | Write and run tests for the python-bsblan library. Use this skill when creating unit tests, working with fixtures, or ensuring code coverage requirements are met. |
Testing python-bsblan
This skill guides you through testing practices for the python-bsblan library.
Test Structure
Tests are located in tests/ and use pytest with async support.
Basic Test Pattern
import pytest
from bsblan import BSBLAN
@pytest.mark.asyncio
async def test_feature_name(mock_bsblan: BSBLAN) -> None:
"""Test description."""
expected_value = "expected"
result = await mock_bsblan.some_method()
assert result == expected_value
Using Fixtures
Test fixtures (JSON responses) are in tests/fixtures/. Common fixtures:
device.json - Device information
state.json - Current state
hot_water_state.json - Hot water state
sensor.json - Sensor readings
Load fixtures using load_fixture(filename: str) -> str from tests/__init__.py.
import json
from tests import load_fixture
raw = load_fixture("device.json")
data = json.loads(raw)
Coverage Requirements
- Total coverage: 95%+ required
- Patch coverage: 100% required (all new/modified code must be fully tested)
Check Coverage
uv run pytest --cov=src/bsblan --cov-report=term-missing
uv run pytest tests/test_your_file.py --cov=src/bsblan --cov-report=term-missing --cov-fail-under=0
uv run pytest --cov=src/bsblan --cov-report=html
Verify New Code is Covered
After adding new methods, always verify coverage:
- Run tests with
--cov-report=term-missing
- Check the "Missing" column shows no line numbers for your new code
- Look for uncovered branches (shown as
line->branch like 382->386)
Example output showing good coverage:
src/bsblan/bsblan.py 426 0 170 2 99% 382->386, 1393->1391
The 382->386 notation means line 382's branch to line 386 isn't covered (an edge case).
GitHub Actions Coverage
CI enforces:
- Total coverage ≥ 95%
- Patch coverage = 100% (Codecov checks new/modified lines)
If CI fails with coverage issues, check the Codecov report in the PR for uncovered lines.
If a line is genuinely untestable (for example defensive guards), mark it with
# pragma: no cover and justify that choice in the PR description.
Running Tests
uv run pytest
uv run pytest tests/test_bsblan.py
uv run pytest -v
uv run pytest tests/test_bsblan.py::test_function_name
Prek Hooks
Always run before committing:
uv run prek run --all-files
This runs:
- Ruff: Linting and formatting (88 char line limit)
- MyPy: Static type checking
- Pylint: Code analysis
Mock Patterns
For API calls, use mock_bsblan fixture and verify calls:
mock_bsblan._request.assert_awaited_with(
base_path="/JS",
data={"Parameter": "1610", "Value": "60.0", "Type": "1"},
)
Fixture Setup and Registration
Define shared fixtures in tests/conftest.py so pytest auto-discovers them.
Use the mock_bsblan pattern for naming and setup:
@pytest.fixture
async def mock_bsblan(
aresponses: ResponsesMockServer,
monkeypatch: Any,
) -> AsyncGenerator[BSBLAN, Any]:
...
Add new JSON payloads in tests/fixtures/ with descriptive, snake_case names
that match the behavior under test.
Mocking HTTP Response Handling
Use monkeypatch with AsyncMock to return fixture payloads when testing
response parsing logic (not only outgoing request arguments):
import json
from unittest.mock import AsyncMock
from tests import load_fixture
request_mock = AsyncMock(return_value=json.loads(load_fixture("state.json")))
monkeypatch.setattr(bsblan, "_request", request_mock)
state = await bsblan.state()
assert state.current_temperature is not None
Testing Lazy Loading
When testing hot water methods, mark param groups as validated to skip network calls:
@pytest.mark.asyncio
async def test_hot_water_no_params_error(monkeypatch: Any) -> None:
"""Test error when no parameters available."""
bsblan = BSBLAN(config, session=session)
bsblan.set_hot_water_cache({})
bsblan._validated_hot_water_groups.add("essential")
with pytest.raises(BSBLANError, match="No essential hot water"):
await bsblan.hot_water_state()
For full integration tests with mocked responses:
bsblan._validated_hot_water_groups.add("config")
bsblan.set_hot_water_cache({"1601": "eco_mode_selection", ...})
Testing Concurrent Access
The library uses asyncio locks for race condition prevention. When testing:
- Locks are created per-section/group automatically
- Access
_section_locks and _hot_water_group_locks dicts if needed
- The double-checked locking pattern prevents duplicate validations
bsblan._section_locks
bsblan._hot_water_group_locks