181 lines
5.9 KiB
Python
181 lines
5.9 KiB
Python
"""Unit-тесты погодного сервиса. Сеть не используется: HTTP-слой подменяется.
|
|
|
|
Запуск: pytest
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
# weather.py лежит внутри каталога агента и не является пакетом,
|
|
# поэтому для тестов добавляем его каталог в sys.path.
|
|
AGENT_DIR = Path(__file__).resolve().parents[1] / "agents" / "custom" / "weather"
|
|
sys.path.insert(0, str(AGENT_DIR))
|
|
|
|
import weather # noqa: E402
|
|
|
|
LOCATION = weather.Location("Казань", "Татарстан", "Россия", 55.7887, 49.1221)
|
|
|
|
GEOCODE_PAYLOAD = {
|
|
"results": [
|
|
{
|
|
"name": "Казань",
|
|
"admin1": "Татарстан",
|
|
"country": "Россия",
|
|
"latitude": 55.78874,
|
|
"longitude": 49.12214,
|
|
}
|
|
]
|
|
}
|
|
|
|
FORECAST_PAYLOAD = {
|
|
"properties": {
|
|
"timeseries": [
|
|
{
|
|
"data": {
|
|
"instant": {
|
|
"details": {
|
|
"air_temperature": 22.2,
|
|
"relative_humidity": 54.0,
|
|
"wind_speed": 4.1,
|
|
"wind_from_direction": 103.0,
|
|
}
|
|
},
|
|
"next_1_hours": {
|
|
"summary": {"symbol_code": "partlycloudy_day"},
|
|
"details": {"precipitation_amount": 0.0},
|
|
},
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clear_geocode_cache():
|
|
"""Кэш геокодера — глобальный, поэтому чистим его перед каждым тестом."""
|
|
weather.geocode.cache_clear()
|
|
yield
|
|
weather.geocode.cache_clear()
|
|
|
|
|
|
# --- geocode ---------------------------------------------------------------
|
|
|
|
def test_geocode_empty_city_raises():
|
|
with pytest.raises(weather.WeatherError) as exc:
|
|
weather.geocode(" ")
|
|
assert exc.value.message == "не указан город"
|
|
|
|
|
|
def test_geocode_not_found(monkeypatch):
|
|
monkeypatch.setattr(weather, "_get", lambda url, params: {"results": []})
|
|
with pytest.raises(weather.WeatherError) as exc:
|
|
weather.geocode("Абракадабрания")
|
|
assert "не найден" in exc.value.message
|
|
|
|
|
|
def test_geocode_builds_location_and_rounds_coordinates(monkeypatch):
|
|
monkeypatch.setattr(weather, "_get", lambda url, params: GEOCODE_PAYLOAD)
|
|
location = weather.geocode("Казань")
|
|
|
|
assert location.title == "Казань, Татарстан, Россия"
|
|
# met.no принимает не более 4 знаков после запятой.
|
|
assert location.latitude == 55.7887
|
|
assert location.longitude == 49.1221
|
|
|
|
|
|
def test_geocode_is_cached(monkeypatch):
|
|
calls: list[str] = []
|
|
|
|
def fake_get(url, params):
|
|
calls.append(params["name"])
|
|
return GEOCODE_PAYLOAD
|
|
|
|
monkeypatch.setattr(weather, "_get", fake_get)
|
|
weather.geocode("Казань")
|
|
weather.geocode("Казань")
|
|
|
|
assert calls == ["Казань"] # второй вызов обслужен из кэша
|
|
|
|
|
|
# --- fetch_weather ---------------------------------------------------------
|
|
|
|
def test_fetch_weather_maps_fields(monkeypatch):
|
|
monkeypatch.setattr(weather, "_get", lambda url, params: FORECAST_PAYLOAD)
|
|
current = weather.fetch_weather(LOCATION)
|
|
|
|
assert current.temperature_c == 22.2
|
|
assert current.humidity_pct == 54
|
|
assert current.wind_ms == 4.1
|
|
assert current.wind_dir_deg == 103
|
|
assert current.precipitation_mm == 0.0
|
|
assert current.description == "переменная облачность"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("symbol", "expected"),
|
|
[("clearsky_night", "ясно"), ("snow", "снег"), ("unknowncode", "нет данных")],
|
|
)
|
|
def test_fetch_weather_strips_symbol_suffix(monkeypatch, symbol, expected):
|
|
payload = {
|
|
"properties": {
|
|
"timeseries": [
|
|
{
|
|
"data": {
|
|
"instant": {
|
|
"details": {
|
|
"air_temperature": 0.0,
|
|
"relative_humidity": 90.0,
|
|
"wind_speed": 1.0,
|
|
"wind_from_direction": 0.0,
|
|
}
|
|
},
|
|
"next_1_hours": {"summary": {"symbol_code": symbol}, "details": {}},
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
monkeypatch.setattr(weather, "_get", lambda url, params: payload)
|
|
assert weather.fetch_weather(LOCATION).description == expected
|
|
|
|
|
|
def test_fetch_weather_bad_payload_raises(monkeypatch):
|
|
monkeypatch.setattr(weather, "_get", lambda url, params: {})
|
|
with pytest.raises(weather.WeatherError):
|
|
weather.fetch_weather(LOCATION)
|
|
|
|
|
|
# --- _get ------------------------------------------------------------------
|
|
|
|
def test_get_does_not_retry_4xx(monkeypatch):
|
|
calls = {"n": 0}
|
|
|
|
class Response:
|
|
status_code = 404
|
|
|
|
class Session:
|
|
def get(self, *args, **kwargs):
|
|
calls["n"] += 1
|
|
return Response()
|
|
|
|
monkeypatch.setattr(weather, "_session", Session())
|
|
with pytest.raises(weather.WeatherError):
|
|
weather._get("https://example.invalid", {})
|
|
|
|
assert calls["n"] == 1 # постоянную 4xx не повторяем
|
|
|
|
|
|
# --- format_summary --------------------------------------------------------
|
|
|
|
def test_format_summary_contains_key_facts():
|
|
current = weather.Weather(22.2, 54, 4.1, 103, 0.0, "переменная облачность")
|
|
text = weather.format_summary(LOCATION, current)
|
|
|
|
assert "Казань, Татарстан, Россия" in text
|
|
assert "переменная облачность" in text
|
|
assert "22.2 °C" in text
|