discordBot/tests/test_pogoda.py
deadzilla b8b1801f23 fix(weather): исправлены баги в погодном функционале
- JSONDecodeError теперь ловится в fetch_weather (был краш при невалидном JSON от API)
- Порывы ветра больше не теряются при отсутствии base wind (gust fallback)
- Устранено дублирование 'м/с' при порывах + отсутствии направления ветра
- Переименован windspeedKmph → wind_speed_mps (ключ хранил м/с, а не км/ч)
- Обновлены тесты, добавлены 6 новых (gust-only, no-duplication, JSONDecodeError)
2026-07-20 23:56:07 +05:00

404 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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) == ""