Project skeleton
This commit is contained in:
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests package for PV Microinverter integration."""
|
||||
56
tests/conftest.py
Normal file
56
tests/conftest.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""Pytest fixtures for PV Microinverter tests."""
|
||||
|
||||
from datetime import datetime
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from pv_microinverter.api import PVMicroinverterApiClient
|
||||
from pv_microinverter.const import (
|
||||
CONF_STATION_ID,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
DEFAULT_UPDATE_INTERVAL,
|
||||
PVMicroinverterData,
|
||||
)
|
||||
from pv_microinverter.coordinator import (
|
||||
PVMicroinverterDataUpdateCoordinator,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_api_client():
|
||||
"""Return a mocked PV Microinverter API client."""
|
||||
client = MagicMock(spec=PVMicroinverterApiClient)
|
||||
client.async_get_data = AsyncMock(
|
||||
return_value=PVMicroinverterData(
|
||||
current_power=500.0,
|
||||
today_energy=2.5,
|
||||
lifetime_energy=150.0,
|
||||
last_updated=datetime.now().isoformat(),
|
||||
)
|
||||
)
|
||||
client.async_check_connection = AsyncMock(return_value=True)
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry():
|
||||
"""Return a mock config entry."""
|
||||
return MagicMock(
|
||||
data={
|
||||
CONF_STATION_ID: "test_station_id",
|
||||
CONF_UPDATE_INTERVAL: DEFAULT_UPDATE_INTERVAL,
|
||||
},
|
||||
entry_id="test_entry_id",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_coordinator(mock_api_client):
|
||||
"""Return a mock coordinator."""
|
||||
coordinator = MagicMock(spec=PVMicroinverterDataUpdateCoordinator)
|
||||
coordinator.api_client = mock_api_client
|
||||
coordinator.data = mock_api_client.async_get_data.return_value
|
||||
coordinator.last_update_success = True
|
||||
coordinator.async_config_entry_first_refresh = AsyncMock()
|
||||
return coordinator
|
||||
206
tests/test_api.py
Normal file
206
tests/test_api.py
Normal file
@@ -0,0 +1,206 @@
|
||||
"""Tests for the PV Microinverter API client."""
|
||||
|
||||
import json
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import aiohttp
|
||||
import pytest
|
||||
from aiohttp import ClientResponseError, ClientSession
|
||||
from custom_components.pv_microinverter.api import (
|
||||
PVMicroinverterApiClient,
|
||||
PVMicroinverterApiClientError,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_session():
|
||||
"""Return a mocked aiohttp client session."""
|
||||
session = MagicMock(spec=ClientSession)
|
||||
session.get = AsyncMock()
|
||||
return session
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def api_client(mock_session):
|
||||
"""Return a new API client with a mocked session."""
|
||||
return PVMicroinverterApiClient(
|
||||
session=mock_session,
|
||||
station_id="test_station_id",
|
||||
base_url="https://api.example.com/v1",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_response():
|
||||
"""Return a mocked API response."""
|
||||
mock = MagicMock()
|
||||
mock.raise_for_status = MagicMock()
|
||||
mock.json = AsyncMock(
|
||||
return_value={
|
||||
"current_power": 500.5,
|
||||
"today_energy": 3.75,
|
||||
"lifetime_energy": 1250.25,
|
||||
"last_updated": "2023-04-01T12:00:00Z",
|
||||
}
|
||||
)
|
||||
return mock
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_get_data_success(api_client, mock_session, mock_response):
|
||||
"""Test successful data retrieval."""
|
||||
# Setup the mock response
|
||||
mock_session.get.return_value = mock_response
|
||||
|
||||
# Call the method
|
||||
data = await api_client.async_get_data()
|
||||
|
||||
# Verify the API call
|
||||
mock_session.get.assert_called_once_with(
|
||||
"https://api.example.com/v1/systems/test_system_id/stats",
|
||||
headers={
|
||||
"Authorization": "Bearer test_api_key",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
|
||||
# Verify the response processing
|
||||
assert data.current_power == 500.5
|
||||
assert data.today_energy == 3.75
|
||||
assert data.lifetime_energy == 1250.25
|
||||
assert data.last_updated == "2023-04-01T12:00:00Z"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_get_data_http_error(api_client, mock_session):
|
||||
"""Test handling of HTTP errors."""
|
||||
# Setup the mock to raise an error
|
||||
error_response = MagicMock()
|
||||
error_response.raise_for_status.side_effect = ClientResponseError(
|
||||
request_info=MagicMock(),
|
||||
history=None,
|
||||
status=401,
|
||||
message="Unauthorized",
|
||||
headers=None,
|
||||
)
|
||||
mock_session.get.return_value = error_response
|
||||
|
||||
# Call the method and expect an exception
|
||||
with pytest.raises(PVMicroinverterApiClientError) as excinfo:
|
||||
await api_client.async_get_data()
|
||||
|
||||
# Verify the error message
|
||||
assert "Error fetching data from API" in str(excinfo.value)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_get_data_connection_error(api_client, mock_session):
|
||||
"""Test handling of connection errors."""
|
||||
# Setup the mock to raise a connection error
|
||||
mock_session.get.side_effect = aiohttp.ClientConnectionError("Connection refused")
|
||||
|
||||
# Call the method and expect an exception
|
||||
with pytest.raises(PVMicroinverterApiClientError) as excinfo:
|
||||
await api_client.async_get_data()
|
||||
|
||||
# Verify the error message
|
||||
assert "Error fetching data from API" in str(excinfo.value)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_get_data_invalid_json(api_client, mock_session, mock_response):
|
||||
"""Test handling of invalid JSON responses."""
|
||||
# Setup the mock to return invalid JSON
|
||||
mock_response.json.side_effect = json.JSONDecodeError("Invalid JSON", "", 0)
|
||||
mock_session.get.return_value = mock_response
|
||||
|
||||
# Call the method and expect an exception
|
||||
with pytest.raises(PVMicroinverterApiClientError) as excinfo:
|
||||
await api_client.async_get_data()
|
||||
|
||||
# Verify the error message
|
||||
assert "Unexpected error occurred" in str(excinfo.value)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_get_data_missing_fields(api_client, mock_session, mock_response):
|
||||
"""Test handling of responses with missing fields."""
|
||||
# Setup the mock to return a response with missing fields
|
||||
mock_response.json.return_value = {"some_other_field": "value"}
|
||||
mock_session.get.return_value = mock_response
|
||||
|
||||
# Call the method - it should handle missing fields gracefully
|
||||
data = await api_client.async_get_data()
|
||||
|
||||
# Verify default values are used
|
||||
assert data.current_power == 0
|
||||
assert data.today_energy == 0
|
||||
assert data.lifetime_energy == 0
|
||||
assert data.last_updated is not None # Should default to current time
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_check_connection_success(api_client, mock_session):
|
||||
"""Test successful connection check."""
|
||||
# Setup the mock response
|
||||
mock_response = MagicMock()
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_session.get.return_value = mock_response
|
||||
|
||||
# Call the method
|
||||
result = await api_client.async_check_connection()
|
||||
|
||||
# Verify the API call
|
||||
mock_session.post.assert_called_once_with(
|
||||
f"{api_client._base_url}/GetStationInfo",
|
||||
headers={
|
||||
"Authorization": "Bearer test_api_key",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
|
||||
# Verify the result
|
||||
assert result is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_check_connection_failure(api_client, mock_session):
|
||||
"""Test failed connection check."""
|
||||
# Setup the mock to raise an error
|
||||
mock_session.get.side_effect = aiohttp.ClientError("Connection error")
|
||||
|
||||
# Call the method
|
||||
result = await api_client.async_check_connection()
|
||||
|
||||
# Verify the result
|
||||
assert result is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_data_with_various_types(api_client):
|
||||
"""Test data processing with various data types."""
|
||||
# Test with string values that should be converted to float
|
||||
data_with_strings = {
|
||||
"current_power": "450.75",
|
||||
"today_energy": "2.5",
|
||||
"lifetime_energy": "1000",
|
||||
"last_updated": "2023-04-01T14:30:00Z",
|
||||
}
|
||||
result = api_client._process_data(data_with_strings)
|
||||
assert result.current_power == 450.75
|
||||
assert result.today_energy == 2.5
|
||||
assert result.lifetime_energy == 1000.0
|
||||
assert result.last_updated == "2023-04-01T14:30:00Z"
|
||||
|
||||
# Test with mixed types
|
||||
data_mixed = {
|
||||
"current_power": 300,
|
||||
"today_energy": 1.5,
|
||||
"lifetime_energy": "750.5",
|
||||
"last_updated": "2023-04-01T14:30:00Z",
|
||||
}
|
||||
result = api_client._process_data(data_mixed)
|
||||
assert result.current_power == 300.0
|
||||
assert result.today_energy == 1.5
|
||||
assert result.lifetime_energy == 750.5
|
||||
assert result.last_updated == "2023-04-01T14:30:00Z"
|
||||
72
tests/test_sensor.py
Normal file
72
tests/test_sensor.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Tests for the PV Microinverter sensor platform."""
|
||||
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
||||
from homeassistant.const import UnitOfEnergy, UnitOfPower
|
||||
|
||||
from pv_microinverter.const import PVMicroinverterData
|
||||
from pv_microinverter.coordinator import (
|
||||
PVMicroinverterDataUpdateCoordinator,
|
||||
)
|
||||
from pv_microinverter.sensor import PVMicroinverterSensor
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sensor_initialization():
|
||||
"""Test sensor initialization."""
|
||||
# Mock data and coordinator
|
||||
mock_data = PVMicroinverterData(
|
||||
current_power=500.0,
|
||||
today_energy=2.5,
|
||||
lifetime_energy=150.0,
|
||||
last_updated=datetime.now().isoformat(),
|
||||
)
|
||||
|
||||
mock_coordinator = MagicMock(spec=PVMicroinverterDataUpdateCoordinator)
|
||||
mock_coordinator.data = mock_data
|
||||
mock_coordinator.last_update_success = True
|
||||
|
||||
# Test current power sensor
|
||||
current_power_sensor = PVMicroinverterSensor(
|
||||
coordinator=mock_coordinator,
|
||||
system_id="test_system",
|
||||
sensor_type="current_power",
|
||||
sensor_info={
|
||||
"name": "Current Power",
|
||||
"icon": "mdi:solar-power",
|
||||
"unit": "W",
|
||||
"device_class": "power",
|
||||
"state_class": "measurement",
|
||||
},
|
||||
)
|
||||
|
||||
# Verify sensor properties
|
||||
assert current_power_sensor.name == "Current Power"
|
||||
assert current_power_sensor.native_unit_of_measurement == UnitOfPower.WATT
|
||||
assert current_power_sensor.device_class == SensorDeviceClass.POWER
|
||||
assert current_power_sensor.state_class == SensorStateClass.MEASUREMENT
|
||||
assert current_power_sensor.native_value == 500.0
|
||||
|
||||
# Test today's energy sensor
|
||||
today_energy_sensor = PVMicroinverterSensor(
|
||||
coordinator=mock_coordinator,
|
||||
system_id="test_system",
|
||||
sensor_type="today_energy",
|
||||
sensor_info={
|
||||
"name": "Today's Energy",
|
||||
"icon": "mdi:solar-power",
|
||||
"unit": "kWh",
|
||||
"device_class": "energy",
|
||||
"state_class": "total_increasing",
|
||||
},
|
||||
)
|
||||
|
||||
# Verify sensor properties
|
||||
assert today_energy_sensor.name == "Today's Energy"
|
||||
assert today_energy_sensor.native_unit_of_measurement == UnitOfEnergy.KILO_WATT_HOUR
|
||||
assert today_energy_sensor.device_class == SensorDeviceClass.ENERGY
|
||||
assert today_energy_sensor.state_class == SensorStateClass.TOTAL_INCREASING
|
||||
assert today_energy_sensor.native_value == 2.5
|
||||
42
tests/test_units.py
Normal file
42
tests/test_units.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import pytest
|
||||
|
||||
from pv_microinverter.units import Dimension, SIUnit
|
||||
|
||||
|
||||
def test_siunit_parse_base_unit():
|
||||
unit = SIUnit.parse("W")
|
||||
# Check that parsing base unit returns the registered unit
|
||||
assert unit.name == "Watt"
|
||||
assert unit.symbol == "W"
|
||||
assert unit.factor == 1
|
||||
|
||||
|
||||
def test_siunit_parse_prefixed_unit():
|
||||
unit = SIUnit.parse("kW")
|
||||
# The expected unit is created by prefix "k" and the base unit "Watt"
|
||||
assert unit.name == "kWatt"
|
||||
assert unit.symbol == "kW"
|
||||
assert unit.factor == 1000 # 1e3 multiplier
|
||||
|
||||
|
||||
def test_dimension_parse_valid():
|
||||
# Parsing a valid dimension string should succeed
|
||||
dim = Dimension.parse("3.5 kW")
|
||||
assert dim.value == "3.5"
|
||||
# Check unit attributes from SIUnit.parse
|
||||
assert dim.unit.name == "kWatt"
|
||||
assert dim.unit.symbol == "kW"
|
||||
# Check conversion to base unit: 3.5 * 1000 = 3500.0
|
||||
assert dim.to_base_unit() == 3500.0
|
||||
|
||||
|
||||
def test_dimension_parse_invalid_format():
|
||||
# Missing space between value and unit should raise a ValueError
|
||||
with pytest.raises(ValueError):
|
||||
Dimension.parse("3.5kW")
|
||||
|
||||
|
||||
def test_siunit_parse_unknown_unit():
|
||||
# Attempting to parse an unknown unit should raise a ValueError
|
||||
with pytest.raises(ValueError):
|
||||
SIUnit.parse("invalid")
|
||||
Reference in New Issue
Block a user