REFACTOR AEMET weather file
This commit is contained in:
677
services/data/tests/test_aemet.py
Normal file
677
services/data/tests/test_aemet.py
Normal file
@@ -0,0 +1,677 @@
|
||||
# ================================================================
|
||||
# services/data/tests/test_aemet.py
|
||||
# ================================================================
|
||||
"""
|
||||
Comprehensive test suite for AEMET weather API client
|
||||
Following the same patterns as test_madrid_opendata.py
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import Mock, patch, AsyncMock
|
||||
import math
|
||||
from typing import Dict, List, Any
|
||||
|
||||
from app.external.aemet import (
|
||||
AEMETClient,
|
||||
WeatherDataParser,
|
||||
SyntheticWeatherGenerator,
|
||||
LocationService,
|
||||
AEMETConstants,
|
||||
WeatherSource,
|
||||
WeatherStation,
|
||||
GeographicBounds
|
||||
)
|
||||
|
||||
# Configure pytest-asyncio
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
class TestAEMETClient:
|
||||
"""Main test class for AEMET API client functionality"""
|
||||
|
||||
@pytest.fixture
|
||||
def client(self):
|
||||
"""Create AEMET client instance for testing"""
|
||||
return AEMETClient()
|
||||
|
||||
@pytest.fixture
|
||||
def madrid_coords(self):
|
||||
"""Standard Madrid coordinates for testing"""
|
||||
return (40.4168, -3.7038) # Madrid city center
|
||||
|
||||
@pytest.fixture
|
||||
def mock_aemet_response(self):
|
||||
"""Mock AEMET API response structure"""
|
||||
return {
|
||||
"datos": "https://opendata.aemet.es/opendata/sh/12345",
|
||||
"metadatos": "https://opendata.aemet.es/opendata/sh/metadata"
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def mock_weather_data(self):
|
||||
"""Mock current weather data from AEMET"""
|
||||
return {
|
||||
"ta": 18.5, # Temperature
|
||||
"prec": 0.0, # Precipitation
|
||||
"hr": 65.0, # Humidity
|
||||
"vv": 12.0, # Wind speed
|
||||
"pres": 1015.2, # Pressure
|
||||
"descripcion": "Despejado"
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def mock_forecast_data(self):
|
||||
"""Mock forecast data from AEMET"""
|
||||
return [{
|
||||
"prediccion": {
|
||||
"dia": [
|
||||
{
|
||||
"fecha": "2025-07-25T00:00:00",
|
||||
"temperatura": {
|
||||
"maxima": 28,
|
||||
"minima": 15
|
||||
},
|
||||
"probPrecipitacion": [
|
||||
{"value": 10, "periodo": "00-24"}
|
||||
],
|
||||
"viento": [
|
||||
{"velocidad": [15], "direccion": ["N"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fecha": "2025-07-26T00:00:00",
|
||||
"temperatura": {
|
||||
"maxima": 30,
|
||||
"minima": 17
|
||||
},
|
||||
"probPrecipitacion": [
|
||||
{"value": 5, "periodo": "00-24"}
|
||||
],
|
||||
"viento": [
|
||||
{"velocidad": [10], "direccion": ["NE"]}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}]
|
||||
|
||||
@pytest.fixture
|
||||
def mock_historical_data(self):
|
||||
"""Mock historical weather data from AEMET"""
|
||||
return [
|
||||
{
|
||||
"fecha": "2025-07-20",
|
||||
"tmax": 25.2,
|
||||
"tmin": 14.8,
|
||||
"prec": 0.0,
|
||||
"hr": 58,
|
||||
"velmedia": 8.5,
|
||||
"presMax": 1018.5,
|
||||
"presMin": 1012.3
|
||||
},
|
||||
{
|
||||
"fecha": "2025-07-21",
|
||||
"tmax": 27.1,
|
||||
"tmin": 16.2,
|
||||
"prec": 2.5,
|
||||
"hr": 72,
|
||||
"velmedia": 12.0,
|
||||
"presMax": 1015.8,
|
||||
"presMin": 1010.1
|
||||
}
|
||||
]
|
||||
|
||||
# ================================================================
|
||||
# CURRENT WEATHER TESTS
|
||||
# ================================================================
|
||||
|
||||
async def test_get_current_weather_success(self, client, madrid_coords, mock_aemet_response, mock_weather_data):
|
||||
"""Test successful current weather retrieval"""
|
||||
lat, lon = madrid_coords
|
||||
|
||||
with patch.object(client, '_get', new_callable=AsyncMock) as mock_get, \
|
||||
patch.object(client, '_fetch_from_url', new_callable=AsyncMock) as mock_fetch:
|
||||
|
||||
mock_get.return_value = mock_aemet_response
|
||||
mock_fetch.return_value = [mock_weather_data]
|
||||
|
||||
result = await client.get_current_weather(lat, lon)
|
||||
|
||||
# Validate result structure
|
||||
assert result is not None, "Should return weather data"
|
||||
assert isinstance(result, dict), "Result should be a dictionary"
|
||||
|
||||
# Check required fields
|
||||
required_fields = ['date', 'temperature', 'precipitation', 'humidity', 'wind_speed', 'pressure', 'description', 'source']
|
||||
for field in required_fields:
|
||||
assert field in result, f"Missing required field: {field}"
|
||||
|
||||
# Validate data types and ranges
|
||||
assert isinstance(result['temperature'], float), "Temperature should be float"
|
||||
assert -20 <= result['temperature'] <= 50, "Temperature should be reasonable"
|
||||
assert isinstance(result['precipitation'], float), "Precipitation should be float"
|
||||
assert result['precipitation'] >= 0, "Precipitation should be non-negative"
|
||||
assert 0 <= result['humidity'] <= 100, "Humidity should be percentage"
|
||||
assert result['wind_speed'] >= 0, "Wind speed should be non-negative"
|
||||
assert result['pressure'] > 900, "Pressure should be reasonable"
|
||||
assert result['source'] == WeatherSource.AEMET.value, "Source should be AEMET"
|
||||
|
||||
print(f"✅ Current weather test passed - Temp: {result['temperature']}°C, Source: {result['source']}")
|
||||
|
||||
async def test_get_current_weather_fallback_to_synthetic(self, client, madrid_coords):
|
||||
"""Test fallback to synthetic data when AEMET API fails"""
|
||||
lat, lon = madrid_coords
|
||||
|
||||
with patch.object(client, '_get', new_callable=AsyncMock) as mock_get:
|
||||
mock_get.return_value = None # Simulate API failure
|
||||
|
||||
result = await client.get_current_weather(lat, lon)
|
||||
|
||||
assert result is not None, "Should return synthetic data"
|
||||
assert result['source'] == WeatherSource.SYNTHETIC.value, "Should use synthetic source"
|
||||
assert isinstance(result['temperature'], float), "Temperature should be float"
|
||||
|
||||
print(f"✅ Synthetic fallback test passed - Source: {result['source']}")
|
||||
|
||||
async def test_get_current_weather_invalid_coordinates(self, client):
|
||||
"""Test current weather with invalid coordinates"""
|
||||
invalid_coords = [
|
||||
(200, 200), # Out of range
|
||||
(-200, -200), # Out of range
|
||||
(0, 0), # Not in Madrid area
|
||||
]
|
||||
|
||||
for lat, lon in invalid_coords:
|
||||
result = await client.get_current_weather(lat, lon)
|
||||
|
||||
# Should still return data (synthetic)
|
||||
assert result is not None, f"Should handle invalid coords ({lat}, {lon})"
|
||||
assert result['source'] == WeatherSource.SYNTHETIC.value, "Should use synthetic for invalid coords"
|
||||
|
||||
print(f"✅ Invalid coordinates test passed")
|
||||
|
||||
# ================================================================
|
||||
# FORECAST TESTS
|
||||
# ================================================================
|
||||
|
||||
async def test_get_forecast_success(self, client, madrid_coords, mock_aemet_response, mock_forecast_data):
|
||||
"""Test successful weather forecast retrieval"""
|
||||
lat, lon = madrid_coords
|
||||
days = 7
|
||||
|
||||
with patch.object(client, '_get', new_callable=AsyncMock) as mock_get, \
|
||||
patch.object(client, '_fetch_from_url', new_callable=AsyncMock) as mock_fetch:
|
||||
|
||||
mock_get.return_value = mock_aemet_response
|
||||
mock_fetch.return_value = mock_forecast_data
|
||||
|
||||
result = await client.get_forecast(lat, lon, days)
|
||||
|
||||
# Validate result structure
|
||||
assert isinstance(result, list), "Result should be a list"
|
||||
assert len(result) == days, f"Should return {days} forecast days"
|
||||
|
||||
# Check first forecast day
|
||||
if result:
|
||||
forecast_day = result[0]
|
||||
|
||||
required_fields = ['forecast_date', 'generated_at', 'temperature', 'precipitation', 'humidity', 'wind_speed', 'description', 'source']
|
||||
for field in required_fields:
|
||||
assert field in forecast_day, f"Missing required field: {field}"
|
||||
|
||||
# Validate data types
|
||||
assert isinstance(forecast_day['forecast_date'], datetime), "Forecast date should be datetime"
|
||||
assert isinstance(forecast_day['temperature'], (int, float)), "Temperature should be numeric"
|
||||
assert isinstance(forecast_day['precipitation'], (int, float)), "Precipitation should be numeric"
|
||||
assert forecast_day['source'] in [WeatherSource.AEMET.value, WeatherSource.SYNTHETIC.value], "Valid source"
|
||||
|
||||
print(f"✅ Forecast test passed - {len(result)} days, Source: {forecast_day['source']}")
|
||||
|
||||
async def test_get_forecast_different_durations(self, client, madrid_coords):
|
||||
"""Test forecast for different time durations"""
|
||||
lat, lon = madrid_coords
|
||||
test_durations = [1, 3, 7, 14]
|
||||
|
||||
for days in test_durations:
|
||||
result = await client.get_forecast(lat, lon, days)
|
||||
|
||||
assert isinstance(result, list), f"Result should be list for {days} days"
|
||||
assert len(result) == days, f"Should return exactly {days} forecast days"
|
||||
|
||||
# Check date progression
|
||||
if len(result) > 1:
|
||||
for i in range(1, len(result)):
|
||||
date_diff = result[i]['forecast_date'] - result[i-1]['forecast_date']
|
||||
assert date_diff.days == 1, "Forecast dates should be consecutive days"
|
||||
|
||||
print(f"✅ Multiple duration forecast test passed")
|
||||
|
||||
async def test_get_forecast_fallback_to_synthetic(self, client, madrid_coords):
|
||||
"""Test forecast fallback to synthetic data"""
|
||||
lat, lon = madrid_coords
|
||||
|
||||
with patch.object(client.location_service, 'get_municipality_code') as mock_municipality:
|
||||
mock_municipality.return_value = None # No municipality found
|
||||
|
||||
result = await client.get_forecast(lat, lon, 7)
|
||||
|
||||
assert isinstance(result, list), "Should return synthetic forecast"
|
||||
assert len(result) == 7, "Should return 7 days"
|
||||
assert all(day['source'] == WeatherSource.SYNTHETIC.value for day in result), "All should be synthetic"
|
||||
|
||||
print(f"✅ Forecast synthetic fallback test passed")
|
||||
|
||||
# ================================================================
|
||||
# HISTORICAL WEATHER TESTS
|
||||
# ================================================================
|
||||
|
||||
async def test_get_historical_weather_success(self, client, madrid_coords, mock_aemet_response, mock_historical_data):
|
||||
"""Test successful historical weather retrieval"""
|
||||
lat, lon = madrid_coords
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=7)
|
||||
|
||||
with patch.object(client, '_get', new_callable=AsyncMock) as mock_get, \
|
||||
patch.object(client, '_fetch_from_url', new_callable=AsyncMock) as mock_fetch:
|
||||
|
||||
mock_get.return_value = mock_aemet_response
|
||||
mock_fetch.return_value = mock_historical_data
|
||||
|
||||
result = await client.get_historical_weather(lat, lon, start_date, end_date)
|
||||
|
||||
# Validate result structure
|
||||
assert isinstance(result, list), "Result should be a list"
|
||||
assert len(result) > 0, "Should return historical data"
|
||||
|
||||
# Check first historical record
|
||||
if result:
|
||||
record = result[0]
|
||||
|
||||
required_fields = ['date', 'temperature', 'precipitation', 'humidity', 'wind_speed', 'pressure', 'description', 'source']
|
||||
for field in required_fields:
|
||||
assert field in record, f"Missing required field: {field}"
|
||||
|
||||
# Validate data types and ranges
|
||||
assert isinstance(record['date'], datetime), "Date should be datetime"
|
||||
assert isinstance(record['temperature'], (int, float, type(None))), "Temperature should be numeric or None"
|
||||
if record['temperature']:
|
||||
assert -30 <= record['temperature'] <= 50, "Temperature should be reasonable"
|
||||
assert record['precipitation'] >= 0, "Precipitation should be non-negative"
|
||||
assert record['source'] == WeatherSource.AEMET.value, "Source should be AEMET"
|
||||
|
||||
print(f"✅ Historical weather test passed - {len(result)} records, Source: {record['source']}")
|
||||
|
||||
async def test_get_historical_weather_date_ranges(self, client, madrid_coords):
|
||||
"""Test historical weather with different date ranges"""
|
||||
lat, lon = madrid_coords
|
||||
end_date = datetime.now()
|
||||
|
||||
test_ranges = [
|
||||
1, # 1 day
|
||||
7, # 1 week
|
||||
30, # 1 month
|
||||
90, # 3 months
|
||||
]
|
||||
|
||||
for days in test_ranges:
|
||||
start_date = end_date - timedelta(days=days)
|
||||
|
||||
result = await client.get_historical_weather(lat, lon, start_date, end_date)
|
||||
|
||||
assert isinstance(result, list), f"Result should be list for {days} days"
|
||||
# Note: Actual count may vary due to chunking and data availability
|
||||
assert len(result) >= 0, f"Should return non-negative count for {days} days"
|
||||
|
||||
if result:
|
||||
# Check date ordering
|
||||
dates = [r['date'] for r in result if 'date' in r]
|
||||
if len(dates) > 1:
|
||||
assert dates == sorted(dates), "Historical dates should be in chronological order"
|
||||
|
||||
print(f"✅ Historical date ranges test passed")
|
||||
|
||||
async def test_get_historical_weather_chunking(self, client, madrid_coords):
|
||||
"""Test historical weather data chunking for large date ranges"""
|
||||
lat, lon = madrid_coords
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=65) # More than 30 days to trigger chunking
|
||||
|
||||
with patch.object(client, '_fetch_historical_chunk', new_callable=AsyncMock) as mock_chunk:
|
||||
mock_chunk.return_value = [] # Empty chunks
|
||||
|
||||
result = await client.get_historical_weather(lat, lon, start_date, end_date)
|
||||
|
||||
# Should have called chunking at least twice (65 days > 30 day limit)
|
||||
assert mock_chunk.call_count >= 2, "Should chunk large date ranges"
|
||||
|
||||
print(f"✅ Historical chunking test passed - {mock_chunk.call_count} chunks")
|
||||
|
||||
# ================================================================
|
||||
# COMPONENT TESTS
|
||||
# ================================================================
|
||||
@pytest.mark.skip_asyncio
|
||||
def test_weather_data_parser(self):
|
||||
"""Test WeatherDataParser functionality"""
|
||||
parser = WeatherDataParser()
|
||||
|
||||
# Test safe_float
|
||||
assert parser.safe_float("15.5", 0.0) == 15.5
|
||||
assert parser.safe_float(None, 10.0) == 10.0
|
||||
assert parser.safe_float("invalid", 5.0) == 5.0
|
||||
assert parser.safe_float(20) == 20.0
|
||||
|
||||
# Test extract_temperature_value
|
||||
assert parser.extract_temperature_value(25.5) == 25.5
|
||||
assert parser.extract_temperature_value("20.0") == 20.0
|
||||
assert parser.extract_temperature_value({"valor": 18.5}) == 18.5
|
||||
assert parser.extract_temperature_value([{"valor": 22.0}]) == 22.0
|
||||
assert parser.extract_temperature_value(None) is None
|
||||
|
||||
# Test generate_weather_description
|
||||
assert "Lluvioso" in parser.generate_weather_description(20, 6.0, 60)
|
||||
assert "Nuboso con lluvia" in parser.generate_weather_description(20, 1.0, 60)
|
||||
assert "Nuboso" in parser.generate_weather_description(20, 0, 85)
|
||||
assert "Soleado y cálido" in parser.generate_weather_description(30, 0, 60)
|
||||
assert "Frío" in parser.generate_weather_description(2, 0, 60)
|
||||
|
||||
print(f"✅ WeatherDataParser tests passed")
|
||||
|
||||
@pytest.mark.skip_asyncio
|
||||
def test_synthetic_weather_generator(self):
|
||||
"""Test SyntheticWeatherGenerator functionality"""
|
||||
generator = SyntheticWeatherGenerator()
|
||||
|
||||
# Test current weather generation
|
||||
current = generator.generate_current_weather()
|
||||
|
||||
assert isinstance(current, dict), "Should return dictionary"
|
||||
assert 'temperature' in current, "Should have temperature"
|
||||
assert 'precipitation' in current, "Should have precipitation"
|
||||
assert current['source'] == WeatherSource.SYNTHETIC.value, "Should be synthetic source"
|
||||
assert isinstance(current['date'], datetime), "Should have datetime"
|
||||
|
||||
# Test forecast generation
|
||||
forecast = generator.generate_forecast_sync(5)
|
||||
|
||||
assert isinstance(forecast, list), "Should return list"
|
||||
assert len(forecast) == 5, "Should return requested days"
|
||||
assert all('forecast_date' in day for day in forecast), "All days should have forecast_date"
|
||||
assert all(day['source'] == WeatherSource.SYNTHETIC.value for day in forecast), "All should be synthetic"
|
||||
|
||||
# Test historical generation
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=7)
|
||||
historical = generator.generate_historical_data(start_date, end_date)
|
||||
|
||||
assert isinstance(historical, list), "Should return list"
|
||||
assert len(historical) == 8, "Should return 8 days (inclusive)"
|
||||
assert all('date' in day for day in historical), "All days should have date"
|
||||
assert all(day['source'] == WeatherSource.SYNTHETIC.value for day in historical), "All should be synthetic"
|
||||
|
||||
print(f"✅ SyntheticWeatherGenerator tests passed")
|
||||
|
||||
@pytest.mark.skip_asyncio
|
||||
def test_location_service(self):
|
||||
"""Test LocationService functionality"""
|
||||
# Test distance calculation
|
||||
madrid_center = (40.4168, -3.7038)
|
||||
madrid_north = (40.4677, -3.5552)
|
||||
|
||||
distance = LocationService.calculate_distance(
|
||||
madrid_center[0], madrid_center[1],
|
||||
madrid_north[0], madrid_north[1]
|
||||
)
|
||||
|
||||
assert isinstance(distance, float), "Distance should be float"
|
||||
assert 0 < distance < 50, "Distance should be reasonable for Madrid area"
|
||||
|
||||
# Test nearest station finding
|
||||
station_id = LocationService.find_nearest_station(madrid_center[0], madrid_center[1])
|
||||
|
||||
assert station_id is not None, "Should find a station"
|
||||
assert station_id in [station.id for station in AEMETConstants.MADRID_STATIONS], "Should be valid station"
|
||||
|
||||
# Test municipality code
|
||||
municipality = LocationService.get_municipality_code(madrid_center[0], madrid_center[1])
|
||||
assert municipality == AEMETConstants.MADRID_MUNICIPALITY_CODE, "Should return Madrid code"
|
||||
|
||||
# Test outside Madrid
|
||||
outside_madrid = LocationService.get_municipality_code(41.0, -4.0) # Outside bounds
|
||||
assert outside_madrid is None, "Should return None for outside Madrid"
|
||||
|
||||
print(f"✅ LocationService tests passed")
|
||||
|
||||
@pytest.mark.skip_asyncio
|
||||
def test_constants_and_enums(self):
|
||||
"""Test constants and enum definitions"""
|
||||
# Test WeatherSource enum
|
||||
assert WeatherSource.AEMET.value == "aemet"
|
||||
assert WeatherSource.SYNTHETIC.value == "synthetic"
|
||||
assert WeatherSource.DEFAULT.value == "default"
|
||||
|
||||
# Test GeographicBounds
|
||||
bounds = AEMETConstants.MADRID_BOUNDS
|
||||
assert bounds.contains(40.4168, -3.7038), "Should contain Madrid center"
|
||||
assert not bounds.contains(41.0, -4.0), "Should not contain coordinates outside Madrid"
|
||||
|
||||
# Test WeatherStation
|
||||
station = AEMETConstants.MADRID_STATIONS[0]
|
||||
assert isinstance(station, WeatherStation), "Should be WeatherStation instance"
|
||||
assert station.id is not None, "Station should have ID"
|
||||
assert station.name is not None, "Station should have name"
|
||||
|
||||
print(f"✅ Constants and enums tests passed")
|
||||
|
||||
# ================================================================
|
||||
# ERROR HANDLING TESTS
|
||||
# ================================================================
|
||||
|
||||
async def test_api_error_handling(self, client, madrid_coords):
|
||||
"""Test handling of various API errors"""
|
||||
lat, lon = madrid_coords
|
||||
|
||||
# Test network error
|
||||
with patch.object(client, '_get', side_effect=Exception("Network error")):
|
||||
result = await client.get_current_weather(lat, lon)
|
||||
assert result['source'] == WeatherSource.SYNTHETIC.value, "Should fallback on network error"
|
||||
|
||||
# Test invalid API response
|
||||
with patch.object(client, '_get', new_callable=AsyncMock) as mock_get:
|
||||
mock_get.return_value = {"error": "Invalid API key"}
|
||||
result = await client.get_current_weather(lat, lon)
|
||||
assert result['source'] == WeatherSource.SYNTHETIC.value, "Should fallback on API error"
|
||||
|
||||
# Test malformed data
|
||||
with patch.object(client, '_get', new_callable=AsyncMock) as mock_get, \
|
||||
patch.object(client, '_fetch_from_url', new_callable=AsyncMock) as mock_fetch:
|
||||
|
||||
mock_get.return_value = {"datos": "http://example.com"}
|
||||
mock_fetch.return_value = [{"invalid": "data"}] # Missing expected fields
|
||||
|
||||
result = await client.get_current_weather(lat, lon)
|
||||
assert result is not None, "Should handle malformed data gracefully"
|
||||
|
||||
print(f"✅ API error handling tests passed")
|
||||
|
||||
async def test_timeout_handling(self, client, madrid_coords):
|
||||
"""Test timeout handling"""
|
||||
lat, lon = madrid_coords
|
||||
|
||||
with patch.object(client, '_get', side_effect=asyncio.TimeoutError("Request timeout")):
|
||||
result = await client.get_current_weather(lat, lon)
|
||||
assert result['source'] == WeatherSource.SYNTHETIC.value, "Should fallback on timeout"
|
||||
|
||||
print(f"✅ Timeout handling test passed")
|
||||
|
||||
# ================================================================
|
||||
# PERFORMANCE TESTS
|
||||
# ================================================================
|
||||
|
||||
async def test_performance_current_weather(self, client, madrid_coords):
|
||||
"""Test current weather performance"""
|
||||
lat, lon = madrid_coords
|
||||
|
||||
start_time = datetime.now()
|
||||
result = await client.get_current_weather(lat, lon)
|
||||
execution_time = (datetime.now() - start_time).total_seconds() * 1000
|
||||
|
||||
assert result is not None, "Should return weather data"
|
||||
assert execution_time < 5000, "Should execute within 5 seconds"
|
||||
|
||||
print(f"✅ Current weather performance test passed - {execution_time:.0f}ms")
|
||||
|
||||
async def test_performance_forecast(self, client, madrid_coords):
|
||||
"""Test forecast performance"""
|
||||
lat, lon = madrid_coords
|
||||
|
||||
start_time = datetime.now()
|
||||
result = await client.get_forecast(lat, lon, 7)
|
||||
execution_time = (datetime.now() - start_time).total_seconds() * 1000
|
||||
|
||||
assert isinstance(result, list), "Should return forecast list"
|
||||
assert len(result) == 7, "Should return 7 days"
|
||||
assert execution_time < 5000, "Should execute within 5 seconds"
|
||||
|
||||
print(f"✅ Forecast performance test passed - {execution_time:.0f}ms")
|
||||
|
||||
async def test_performance_historical(self, client, madrid_coords):
|
||||
"""Test historical weather performance"""
|
||||
lat, lon = madrid_coords
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=7)
|
||||
|
||||
start_time = datetime.now()
|
||||
result = await client.get_historical_weather(lat, lon, start_date, end_date)
|
||||
execution_time = (datetime.now() - start_time).total_seconds() * 1000
|
||||
|
||||
assert isinstance(result, list), "Should return historical list"
|
||||
assert execution_time < 10000, "Should execute within 10 seconds (allowing for API calls)"
|
||||
|
||||
print(f"✅ Historical performance test passed - {execution_time:.0f}ms")
|
||||
|
||||
# ================================================================
|
||||
# INTEGRATION TESTS
|
||||
# ================================================================
|
||||
|
||||
async def test_real_aemet_api_access(self, client, madrid_coords):
|
||||
"""Test actual AEMET API access (if API key is available)"""
|
||||
lat, lon = madrid_coords
|
||||
|
||||
try:
|
||||
# Test current weather
|
||||
current_result = await client.get_current_weather(lat, lon)
|
||||
assert current_result is not None, "Should get current weather"
|
||||
|
||||
if current_result['source'] == WeatherSource.AEMET.value:
|
||||
print(f"🎉 SUCCESS: Got real AEMET current weather data!")
|
||||
print(f" Temperature: {current_result['temperature']}°C")
|
||||
print(f" Description: {current_result['description']}")
|
||||
else:
|
||||
print(f"ℹ️ Got synthetic current weather (API key may not be configured)")
|
||||
|
||||
# Test forecast
|
||||
forecast_result = await client.get_forecast(lat, lon, 3)
|
||||
assert len(forecast_result) == 3, "Should get 3-day forecast"
|
||||
|
||||
if forecast_result[0]['source'] == WeatherSource.AEMET.value:
|
||||
print(f"🎉 SUCCESS: Got real AEMET forecast data!")
|
||||
print(f" Tomorrow: {forecast_result[1]['temperature']}°C - {forecast_result[1]['description']}")
|
||||
else:
|
||||
print(f"ℹ️ Got synthetic forecast (API key may not be configured)")
|
||||
|
||||
# Test historical (last week)
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=7)
|
||||
historical_result = await client.get_historical_weather(lat, lon, start_date, end_date)
|
||||
|
||||
assert isinstance(historical_result, list), "Should get historical data"
|
||||
|
||||
real_historical = [r for r in historical_result if r['source'] == WeatherSource.AEMET.value]
|
||||
if real_historical:
|
||||
print(f"🎉 SUCCESS: Got real AEMET historical data!")
|
||||
print(f" Records: {len(real_historical)} real + {len(historical_result) - len(real_historical)} synthetic")
|
||||
else:
|
||||
print(f"ℹ️ Got synthetic historical data (API limitations or key issues)")
|
||||
|
||||
print(f"✅ Real AEMET API integration test completed")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ AEMET API integration test failed: {e}")
|
||||
# This is acceptable if API key is not configured
|
||||
|
||||
async def test_data_consistency(self, client, madrid_coords):
|
||||
"""Test data consistency across different methods"""
|
||||
lat, lon = madrid_coords
|
||||
|
||||
# Get current weather
|
||||
current = await client.get_current_weather(lat, lon)
|
||||
|
||||
# Get today's forecast
|
||||
forecast = await client.get_forecast(lat, lon, 1)
|
||||
today_forecast = forecast[0] if forecast else None
|
||||
|
||||
if current and today_forecast:
|
||||
# Temperature should be somewhat consistent
|
||||
temp_diff = abs(current['temperature'] - today_forecast['temperature'])
|
||||
assert temp_diff < 15, "Current and forecast temperature should be reasonably consistent"
|
||||
|
||||
# Both should use same source type preference
|
||||
if current['source'] == WeatherSource.AEMET.value:
|
||||
assert today_forecast['source'] == WeatherSource.AEMET.value, "Should use consistent data sources"
|
||||
|
||||
print(f"✅ Data consistency test passed")
|
||||
|
||||
|
||||
# ================================================================
|
||||
# STANDALONE TEST FUNCTIONS
|
||||
# ================================================================
|
||||
|
||||
async def run_manual_test():
|
||||
"""Manual test function that can be run directly"""
|
||||
print("="*60)
|
||||
print("AEMET WEATHER CLIENT TEST - JULY 2025")
|
||||
print("="*60)
|
||||
|
||||
client = AEMETClient()
|
||||
madrid_lat, madrid_lon = 40.4168, -3.7038 # Madrid center
|
||||
|
||||
print(f"\n=== Testing Madrid Weather ({madrid_lat}, {madrid_lon}) ===")
|
||||
|
||||
# Test current weather
|
||||
print(f"\n1. Testing Current Weather...")
|
||||
current = await client.get_current_weather(madrid_lat, madrid_lon)
|
||||
if current:
|
||||
print(f" Temperature: {current['temperature']}°C")
|
||||
print(f" Description: {current['description']}")
|
||||
print(f" Humidity: {current['humidity']}%")
|
||||
print(f" Wind: {current['wind_speed']} km/h")
|
||||
print(f" Source: {current['source']}")
|
||||
|
||||
# Test forecast
|
||||
print(f"\n2. Testing 7-Day Forecast...")
|
||||
forecast = await client.get_forecast(madrid_lat, madrid_lon, 7)
|
||||
if forecast:
|
||||
print(f" Forecast days: {len(forecast)}")
|
||||
print(f" Tomorrow: {forecast[1]['temperature']}°C - {forecast[1]['description']}")
|
||||
print(f" Source: {forecast[0]['source']}")
|
||||
|
||||
# Test historical
|
||||
print(f"\n3. Testing Historical Weather (last 7 days)...")
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=7)
|
||||
historical = await client.get_historical_weather(madrid_lat, madrid_lon, start_date, end_date)
|
||||
if historical:
|
||||
print(f" Historical records: {len(historical)}")
|
||||
if historical:
|
||||
real_count = len([r for r in historical if r['source'] == WeatherSource.AEMET.value])
|
||||
synthetic_count = len(historical) - real_count
|
||||
print(f" Real data: {real_count}, Synthetic: {synthetic_count}")
|
||||
|
||||
print(f"\n✅ Manual test completed!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# If run directly, execute manual test
|
||||
asyncio.run(run_manual_test())
|
||||
Reference in New Issue
Block a user