- JSONDecodeError теперь ловится в fetch_weather (был краш при невалидном JSON от API) - Порывы ветра больше не теряются при отсутствии base wind (gust fallback) - Устранено дублирование 'м/с' при порывах + отсутствии направления ветра - Переименован windspeedKmph → wind_speed_mps (ключ хранил м/с, а не км/ч) - Обновлены тесты, добавлены 6 новых (gust-only, no-duplication, JSONDecodeError)
404 lines
16 KiB
Python
404 lines
16 KiB
Python
import pytest
|
||
from utils.pogoda import (
|
||
pressure_to_mmhg,
|
||
wmo_to_russian,
|
||
yandex_condition_to_russian,
|
||
format_weather_data_for_console,
|
||
get_weather_description,
|
||
)
|
||
|
||
|
||
class TestFormatWeatherDataForConsole:
|
||
"""Тесты функции format_weather_data_for_console()."""
|
||
|
||
def test_format_valid_data(self) -> None:
|
||
"""Полные данные должны быть отформатированы корректно."""
|
||
data = {
|
||
"current_condition": [
|
||
{
|
||
"temp_C": 25,
|
||
"FeelsLikeC": 26,
|
||
"weatherDesc": [{"value": "Ясно"}],
|
||
"humidity": 45,
|
||
"wind_speed_mps": 5.0, # м/с от Яндекса
|
||
"wind_gust": 8.0,
|
||
"wind_dir": "n",
|
||
"pressure": 735.0, # уже в мм рт. ст.
|
||
}
|
||
]
|
||
}
|
||
|
||
result = format_weather_data_for_console(data)
|
||
|
||
assert isinstance(result, list), "Результат должен быть списком строк"
|
||
assert len(result) == 5, "Должно быть 5 полей погоды"
|
||
assert "Температура: 25°C (ощущается как 26°C)" in result[0]
|
||
assert "Описание: Ясно" in result[1]
|
||
assert "Влажность: 45%" in result[2]
|
||
assert "Ветер: 5.0 (порывы 8.0), северный м/с" in result[3]
|
||
assert "Давление: 735.0 мм рт. ст." in result[4]
|
||
|
||
def test_format_empty_data(self) -> None:
|
||
"""Пустые данные должны возвращать None."""
|
||
data = {"current_condition": [{}]}
|
||
|
||
result = format_weather_data_for_console(data)
|
||
|
||
assert result is None, "Пустые данные должны возвращать None"
|
||
|
||
def test_format_missing_current_condition(self) -> None:
|
||
"""Отсутствие current_condition должно вернуть None."""
|
||
data = {}
|
||
|
||
result = format_weather_data_for_console(data)
|
||
|
||
assert result is None, "Отсутствие current_condition должно вернуть None"
|
||
|
||
def test_format_with_dashes(self) -> None:
|
||
"""None значения должны отображаться как '—'."""
|
||
data = {
|
||
"current_condition": [
|
||
{
|
||
"temp_C": None,
|
||
"FeelsLikeC": None,
|
||
"weatherDesc": [{"value": "Неизвестно"}],
|
||
"humidity": None,
|
||
"wind_speed_mps": None,
|
||
"wind_gust": None,
|
||
"pressure": None,
|
||
}
|
||
]
|
||
}
|
||
|
||
result = format_weather_data_for_console(data)
|
||
|
||
assert isinstance(result, list), "Результат должен быть списком строк"
|
||
assert "Температура: —°C (ощущается как —°C)" in result[0]
|
||
assert "Описание: Неизвестно" in result[1]
|
||
assert "Влажность: —%" in result[2]
|
||
assert "Ветер: — м/с" in result[3]
|
||
assert "Давление: — мм рт. ст." in result[4]
|
||
|
||
def test_format_negative_temperature(self) -> None:
|
||
"""Отрицательная температура должна отображаться корректно."""
|
||
data = {
|
||
"current_condition": [
|
||
{
|
||
"temp_C": -5,
|
||
"FeelsLikeC": -10,
|
||
"weatherDesc": [{"value": "Снег"}],
|
||
"humidity": 80,
|
||
"wind_speed_mps": 5,
|
||
"wind_gust": 10,
|
||
"wind_dir": "n",
|
||
"pressure": 720.0,
|
||
}
|
||
]
|
||
}
|
||
|
||
result = format_weather_data_for_console(data)
|
||
|
||
assert isinstance(result, list), "Результат должен быть списком строк"
|
||
assert "Температура: -5°C (ощущается как -10°C)" in result[0]
|
||
|
||
def test_format_without_gust_and_dir(self) -> None:
|
||
"""Без порывов и направления ветра — базовый формат."""
|
||
data = {
|
||
"current_condition": [
|
||
{
|
||
"temp_C": 17.3,
|
||
"FeelsLikeC": 16.8,
|
||
"weatherDesc": [{"value": "Облачно"}],
|
||
"humidity": 87.5,
|
||
"wind_speed_mps": 2.1, # м/с
|
||
"pressure": 750.0,
|
||
}
|
||
]
|
||
}
|
||
|
||
result = format_weather_data_for_console(data)
|
||
assert "Температура: 17.3°C" in result[0]
|
||
assert "Ветер: 2.1 м/с" in result[3]
|
||
|
||
def test_format_all_wind_directions(self) -> None:
|
||
"""Все направления ветра должны переводиться корректно."""
|
||
directions = [
|
||
("n", "северный"),
|
||
("ne", "северо-восточный"),
|
||
("e", "восточный"),
|
||
("se", "юго-восточный"),
|
||
("s", "южный"),
|
||
("sw", "юго-западный"),
|
||
("w", "западный"),
|
||
("nw", "северо-западный"),
|
||
]
|
||
|
||
for code, expected in directions:
|
||
data = {
|
||
"current_condition": [
|
||
{
|
||
"temp_C": 20,
|
||
"FeelsLikeC": 18,
|
||
"weatherDesc": [{"value": "Ясно"}],
|
||
"humidity": 50,
|
||
"wind_speed_mps": 3.0,
|
||
"wind_dir": code,
|
||
"pressure": 750.0,
|
||
}
|
||
]
|
||
}
|
||
result = format_weather_data_for_console(data)
|
||
assert expected in result[3], f"Направление {code} → {expected} не найдено"
|
||
|
||
def test_format_gust_only_no_base_wind(self) -> None:
|
||
"""При отсутствии base wind порывы ветра всё равно показываются."""
|
||
data = {
|
||
"current_condition": [
|
||
{
|
||
"temp_C": 20,
|
||
"FeelsLikeC": 18,
|
||
"weatherDesc": [{"value": "Ясно"}],
|
||
"humidity": 50,
|
||
"wind_speed_mps": None,
|
||
"wind_gust": 10.5,
|
||
"wind_dir": "n",
|
||
"pressure": 750.0,
|
||
}
|
||
]
|
||
}
|
||
|
||
result = format_weather_data_for_console(data)
|
||
assert "Ветер: порывы 10.5, северный м/с" in result[3]
|
||
|
||
def test_format_gust_only_no_duplication(self) -> None:
|
||
"""Порывы без направления и base wind — 'м/с' не дублируется."""
|
||
data = {
|
||
"current_condition": [
|
||
{
|
||
"temp_C": 20,
|
||
"FeelsLikeC": 18,
|
||
"weatherDesc": [{"value": "Ясно"}],
|
||
"humidity": 50,
|
||
"wind_speed_mps": None,
|
||
"wind_gust": 10.5,
|
||
"wind_dir": None,
|
||
"pressure": 750.0,
|
||
}
|
||
]
|
||
}
|
||
|
||
result = format_weather_data_for_console(data)
|
||
assert "Ветер: порывы 10.5 м/с" in result[3]
|
||
assert result[3].count("м/с") == 1
|
||
|
||
def test_format_base_wind_with_gust_no_dir(self) -> None:
|
||
"""Base wind + порывы без направления — 'м/с' не дублируется."""
|
||
data = {
|
||
"current_condition": [
|
||
{
|
||
"temp_C": 20,
|
||
"FeelsLikeC": 18,
|
||
"weatherDesc": [{"value": "Ясно"}],
|
||
"humidity": 50,
|
||
"wind_speed_mps": 5.0,
|
||
"wind_gust": 8.0,
|
||
"wind_dir": None,
|
||
"pressure": 750.0,
|
||
}
|
||
]
|
||
}
|
||
|
||
result = format_weather_data_for_console(data)
|
||
assert "Ветер: 5.0 (порывы 8.0) м/с" in result[3]
|
||
assert result[3].count("м/с") == 1
|
||
|
||
|
||
class TestPressureToMMHG:
|
||
"""Тесты функции pressure_to_mmhg() — конвертация давления из мб в мм рт. ст."""
|
||
|
||
@pytest.mark.parametrize(
|
||
"mb, expected",
|
||
[
|
||
(1013, 759.8),
|
||
(1000, 750.1),
|
||
(980, 735.1),
|
||
(1030, 772.6),
|
||
],
|
||
)
|
||
def test_pressure_valid(self, mb, expected) -> None:
|
||
"""Валидные числовые значения должны конвертироваться корректно."""
|
||
assert pressure_to_mmhg(mb) == expected
|
||
|
||
@pytest.mark.parametrize(
|
||
"mb, expected",
|
||
[
|
||
("1013", 759.8),
|
||
("1000", 750.1),
|
||
("980", 735.1),
|
||
],
|
||
)
|
||
def test_pressure_string(self, mb, expected) -> None:
|
||
"""Строка-число должна конвертироваться корректно."""
|
||
assert pressure_to_mmhg(mb) == expected
|
||
|
||
@pytest.mark.parametrize(
|
||
"input_value, expected",
|
||
[
|
||
("—", "—"),
|
||
(None, "—"),
|
||
("", "—"),
|
||
],
|
||
)
|
||
def test_pressure_invalid(self, input_value, expected) -> None:
|
||
"""Невалидные значения должны возвращать '—'."""
|
||
assert pressure_to_mmhg(input_value) == expected
|
||
|
||
def test_pressure_non_numeric_string(self) -> None:
|
||
"""Невалидная строка должна возвращать '—'."""
|
||
assert pressure_to_mmhg("abc") == "—"
|
||
|
||
def test_pressure_zero(self) -> None:
|
||
"""Нулевое значение — корректно конвертируется в 0.0."""
|
||
assert pressure_to_mmhg(0) == 0.0
|
||
|
||
def test_pressure_negative(self) -> None:
|
||
"""Отрицательное значение должно конвертироваться."""
|
||
assert pressure_to_mmhg(-100) == -75.0
|
||
|
||
def test_pressure_float_string(self) -> None:
|
||
"""Строка с десятичной точкой должна конвертироваться."""
|
||
assert pressure_to_mmhg("1013.25") == 760.0
|
||
|
||
def test_pressure_very_large(self) -> None:
|
||
"""Очень большое значение должно работать."""
|
||
assert pressure_to_mmhg(999999) == 750061.2
|
||
|
||
|
||
class TestWmoToRussian:
|
||
"""Тесты функции wmo_to_russian() — перевод WMO кодов погоды.
|
||
|
||
Оставлен для обратной совместимости.
|
||
"""
|
||
|
||
@pytest.mark.parametrize(
|
||
"code, expected",
|
||
[
|
||
(0, "Ясно"),
|
||
(1, "Преимущественно ясно"),
|
||
(2, "Переменная облачность"),
|
||
(3, "Пасмурно"),
|
||
(45, "Туман"),
|
||
(48, "Изморозь"),
|
||
(51, "Лёгкая морось"),
|
||
(53, "Морось"),
|
||
(55, "Сильная морось"),
|
||
(56, "Ледяная морось"),
|
||
(57, "Сильная ледяная морось"),
|
||
(61, "Небольшой дождь"),
|
||
(63, "Дождь"),
|
||
(65, "Сильный дождь"),
|
||
(66, "Ледяной дождь"),
|
||
(67, "Сильный ледяной дождь"),
|
||
(71, "Небольшой снег"),
|
||
(73, "Снег"),
|
||
(75, "Сильный снег"),
|
||
(77, "Снежная крупа"),
|
||
(80, "Небольшой ливень"),
|
||
(81, "Ливень"),
|
||
(82, "Сильный ливень"),
|
||
(85, "Снежный ливень"),
|
||
(86, "Сильный снежный ливень"),
|
||
(95, "Гроза"),
|
||
(96, "Гроза с градом"),
|
||
(99, "Сильная гроза с градом"),
|
||
],
|
||
)
|
||
def test_wmo_known(self, code, expected) -> None:
|
||
"""Известные WMO коды должны возвращать ожидаемый перевод."""
|
||
assert wmo_to_russian(code) == expected
|
||
|
||
def test_wmo_unknown(self) -> None:
|
||
"""Неизвестный код должен возвращать 'Неизвестно'."""
|
||
assert wmo_to_russian(999) == "Неизвестно"
|
||
|
||
def test_wmo_negative_code(self) -> None:
|
||
"""Отрицательный код должен возвращать 'Неизвестно'."""
|
||
assert wmo_to_russian(-1) == "Неизвестно"
|
||
|
||
def test_wmo_none(self) -> None:
|
||
"""None должен возвращать 'Неизвестно'."""
|
||
assert wmo_to_russian(None) == "Неизвестно"
|
||
|
||
def test_wmo_large_code(self) -> None:
|
||
"""Очень большой код должен возвращать 'Неизвестно'."""
|
||
assert wmo_to_russian(9999) == "Неизвестно"
|
||
|
||
def test_wmo_float_code(self) -> None:
|
||
"""Дробный код — не найдётся в mapping."""
|
||
assert wmo_to_russian(1.5) == "Неизвестно"
|
||
|
||
|
||
class TestYandexConditionToRussian:
|
||
"""Тесты функции yandex_condition_to_russian() — перевод Яндекс condition."""
|
||
|
||
@pytest.mark.parametrize(
|
||
"condition, expected",
|
||
[
|
||
("clear", "Ясно"),
|
||
("partly_cloudy", "Переменная облачность"),
|
||
("cloudy", "Облачно"),
|
||
("overcast", "Пасмурно"),
|
||
("light_rain", "Небольшой дождь"),
|
||
("rain", "Дождь"),
|
||
("heavy_rain", "Сильный дождь"),
|
||
("drizzle", "Морось"),
|
||
("heavy_showers", "Сильные осадки"),
|
||
("thunderstorm", "Гроза"),
|
||
("thunderstorm_with_rain", "Гроза с дождём"),
|
||
("thunderstorm_with_heavy_rain", "Сильная гроза с дождём"),
|
||
("thunderstorm_with_hail", "Гроза с градом"),
|
||
("snow_showers", "Снежные осадки"),
|
||
("light_snow", "Небольшой снег"),
|
||
("snow", "Снег"),
|
||
("heavy_snow", "Сильный снег"),
|
||
("snowstorm", "Метель"),
|
||
("blizzard", "Буран"),
|
||
("fog", "Туман"),
|
||
],
|
||
)
|
||
def test_yandex_known(self, condition, expected) -> None:
|
||
"""Известные Яндекс condition должны возвращать ожидаемый перевод."""
|
||
assert yandex_condition_to_russian(condition) == expected
|
||
|
||
def test_yandex_unknown(self) -> None:
|
||
"""Неизвестный condition должен возвращать 'Неизвестно'."""
|
||
assert yandex_condition_to_russian("unknown_condition") == "Неизвестно"
|
||
|
||
def test_yandex_none(self) -> None:
|
||
"""None должен возвращать 'Неизвестно'."""
|
||
assert yandex_condition_to_russian(None) == "Неизвестно"
|
||
|
||
|
||
class TestGetWeatherDescription:
|
||
"""Тесты функции get_weather_description()."""
|
||
|
||
def test_valid_description(self) -> None:
|
||
"""Нормальный weatherDesc -> значение."""
|
||
current = {"weatherDesc": [{"value": "Ясно"}]}
|
||
assert get_weather_description(current) == "Ясно"
|
||
|
||
def test_empty_current(self) -> None:
|
||
"""Пустой current -> '—'."""
|
||
current = {}
|
||
assert get_weather_description(current) == "—"
|
||
|
||
def test_weather_desc_none(self) -> None:
|
||
"""weatherDesc с None -> '—'."""
|
||
current = {"weatherDesc": None}
|
||
assert get_weather_description(current) == "—"
|
||
|
||
def test_empty_value(self) -> None:
|
||
"""Пустая строка в value -> '—'."""
|
||
current = {"weatherDesc": [{"value": ""}]}
|
||
assert get_weather_description(current) == "—"
|