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."""
# Arrange
expected_value = "expected"
# Act
result = await mock_bsblan.some_method()
# Assert
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 the load_fixture helper from conftest.py.
Coverage Requirements
- •Total coverage: 95%+ required
- •Patch coverage: 100% required (all new/modified code must be fully tested)
Check Coverage
# Full coverage report uv run pytest --cov=src/bsblan --cov-report=term-missing # Coverage for specific test file (useful during development) uv run pytest tests/test_your_file.py --cov=src/bsblan --cov-report=term-missing --cov-fail-under=0 # HTML report for detailed analysis uv run pytest --cov=src/bsblan --cov-report=html # Then open htmlcov/index.html in browser
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->branchlike382->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.
Running Tests
# Run all tests uv run pytest # Run specific test file uv run pytest tests/test_bsblan.py # Run with verbose output uv run pytest -v # Run specific test uv run pytest tests/test_bsblan.py::test_function_name
Pre-commit Hooks
Always run before committing:
uv run pre-commit run --all-files
This runs:
- •Ruff: Linting and formatting (88 char line limit)
- •MyPy: Static type checking
- •Pylint: Code analysis
- •Pytest: Test execution with coverage
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"},
)
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)
# Set empty cache and mark group as validated
bsblan.set_hot_water_cache({})
bsblan._validated_hot_water_groups.add("essential") # Skip validation
with pytest.raises(BSBLANError, match="No essential hot water"):
await bsblan.hot_water_state()
For full integration tests with mocked responses:
# Mark group as validated to use cached params
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_locksand_hot_water_group_locksdicts if needed - •The double-checked locking pattern prevents duplicate validations
# Locks are stored in these dictionaries:
bsblan._section_locks # {"heating": Lock(), "sensor": Lock(), ...}
bsblan._hot_water_group_locks # {"essential": Lock(), ...}