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