Добавлены интеграционные тесты: загрузка когов, команды !cat и !stats, утилиты (9 тестов, итого 239)

This commit is contained in:
deadzilla 2026-07-08 00:27:56 +05:00
parent a5ea2086cd
commit a063abcd17

154
tests/test_integration.py Normal file
View File

@ -0,0 +1,154 @@
"""Интеграционные тесты — проверка взаимодействия компонентов без внешних API.
Тестируют реальные объекты (без моков на уровне команд),
но мокают только сетевые вызовы.
"""
import pytest
import requests
from unittest.mock import AsyncMock, MagicMock, patch
@pytest.fixture
async def loaded_bot():
"""Загрузить все ког-модули в бота."""
import discord
from discord.ext import commands
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix="!", intents=intents)
from commands import ALL_COMMANDS
for cog_class in ALL_COMMANDS:
await bot.add_cog(cog_class())
return bot
class TestCogLoading:
"""Проверка загрузки ког-модулей."""
@pytest.mark.asyncio
async def test_all_cogs_load(self, loaded_bot) -> None:
"""Все ког-модули должны загружаться без ошибок."""
from commands import ALL_COMMANDS
assert len(loaded_bot.cogs) == len(ALL_COMMANDS)
@pytest.mark.asyncio
async def test_commands_registered(self, loaded_bot) -> None:
"""Все команды должны быть зарегистрированы."""
command_names = {cmd.name for cmd in loaded_bot.commands if cmd.cog is not None}
expected = {"pg", "nw", "morning", "cat", "hp", "stats", "status"}
assert command_names == expected
class TestCommandFlow:
"""Проверка полного потока команд (без сетевых вызовов)."""
@pytest.mark.asyncio
async def test_cat_command_success(self, loaded_bot) -> None:
"""Команда !cat должна отправить embed с котиком при успешном ответе API."""
with patch("commands.cat.fetch_cat", new_callable=AsyncMock) as mock_fetch:
mock_fetch.return_value = "https://example.com/cat.jpg"
mock_ctx = MagicMock()
mock_ctx.author.name = "TestUser"
mock_ctx.send = AsyncMock(return_value=None)
cat_cmd = loaded_bot.get_command("cat")
assert cat_cmd is not None
await cat_cmd.callback(cat_cmd.cog, mock_ctx)
mock_ctx.send.assert_awaited_once()
embed = mock_ctx.send.call_args[1]["embed"]
assert embed.title == "Котик для тебя!"
assert embed.image.url == "https://example.com/cat.jpg"
@pytest.mark.asyncio
async def test_cat_command_failure(self, loaded_bot) -> None:
"""Команда !cat при ошибке API должна отправить fallback сообщение."""
with patch("commands.cat.fetch_cat", new_callable=AsyncMock) as mock_fetch:
mock_fetch.return_value = None
mock_ctx = MagicMock()
mock_ctx.author.name = "TestUser"
mock_ctx.send = AsyncMock(return_value=None)
cat_cmd = loaded_bot.get_command("cat")
await cat_cmd.callback(cat_cmd.cog, mock_ctx)
mock_ctx.send.assert_awaited_once()
content = mock_ctx.send.call_args[0][0]
assert "Не удалось получить котика" in content
@pytest.mark.asyncio
async def test_stats_command_flow(self) -> None:
"""Команда !stats должна показать реальную статистику бота."""
import discord
from discord.ext import commands
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix="!", intents=intents)
from commands.stats import Stats
await bot.add_cog(Stats())
mock_ctx = MagicMock()
guild = MagicMock()
guild.channels = [] # нет CategoryChannel
guild.member_count = 100
mock_ctx.bot = MagicMock()
mock_ctx.bot.guilds = [guild]
mock_ctx.bot.latency = 0.050
mock_ctx.send = AsyncMock(return_value=None)
stats_cmd = bot.get_command("stats")
await stats_cmd.callback(stats_cmd.cog, mock_ctx)
mock_ctx.send.assert_awaited_once()
embed = mock_ctx.send.call_args[1]["embed"]
fields = {f.name: f.value for f in embed.fields}
assert fields["Серверов"] == "1"
assert fields["Каналов"] == "0"
assert fields["Пользователей"] == "100"
assert fields["Пинг"] == "50.0 мс"
class TestUtilityFunctions:
"""Проверка утилит без моков."""
def test_wmo_codes_mapping(self) -> None:
"""wmo_to_russian должен переводить известные коды."""
from utils.pogoda import wmo_to_russian
assert wmo_to_russian(0) == "Ясно"
assert wmo_to_russian(3) == "Пасмурно"
assert wmo_to_russian(51) == "Лёгкая морось"
assert wmo_to_russian(None) == "Неизвестно"
def test_pressure_conversion(self) -> None:
"""pressure_to_mmhg должен конвертировать mb в мм рт. ст."""
from utils.pogoda import pressure_to_mmhg
assert pressure_to_mmhg(1013) == 759.8 # 1013 * 0.750062 = 759.81
assert pressure_to_mmhg("") == ""
assert pressure_to_mmhg(None) == ""
def test_title_truncation(self) -> None:
"""truncate_title должен обрезать длинные заголовки."""
from utils.news import truncate_title
short = "Короткий заголовок"
assert truncate_title(short) == short
long_title = "A" * 100
assert len(truncate_title(long_title, 20)) == 23 # 17 + "..."
def test_embed_text_truncation(self) -> None:
"""truncate_embed_text должен обрезать до 4096 символов."""
from utils.news import truncate_embed_text, truncate_embed_field
long_text = "A" * 5000
assert len(truncate_embed_text(long_text)) == 4096
assert truncate_embed_text(long_text).endswith("...")
assert len(truncate_embed_field(long_text)) == 1024