Files
telegram-helper-bot/tests/test_main.py
Andrey 3d6b4353f9
All checks were successful
CI pipeline / Test & Code Quality (push) Successful in 34s
Refactor imports across multiple files to improve code organization and readability.
2026-02-28 23:24:25 +03:00

185 lines
7.5 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.
"""
Тесты для helper_bot.main: start_bot_with_retry, start_bot.
"""
import asyncio
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from helper_bot.main import start_bot, start_bot_with_retry
@pytest.mark.unit
@pytest.mark.asyncio
class TestStartBotWithRetry:
"""Тесты для start_bot_with_retry."""
async def test_success_on_first_try_exits_immediately(
self, mock_bot, mock_dispatcher
):
"""При успешном start_polling с первой попытки цикл завершается без повторов."""
mock_dispatcher.start_polling = AsyncMock()
await start_bot_with_retry(mock_bot, mock_dispatcher, max_retries=3)
mock_dispatcher.start_polling.assert_awaited_once_with(
mock_bot, skip_updates=True
)
@patch("helper_bot.main.asyncio.sleep", new_callable=AsyncMock)
async def test_network_error_retries_then_succeeds(
self, mock_sleep, mock_bot, mock_dispatcher
):
"""При сетевой ошибке выполняется повтор с задержкой, затем успех."""
mock_dispatcher.start_polling = AsyncMock(
side_effect=[ConnectionError("connection reset"), None]
)
await start_bot_with_retry(
mock_bot, mock_dispatcher, max_retries=3, base_delay=0.1
)
assert mock_dispatcher.start_polling.await_count == 2
mock_sleep.assert_awaited_once()
# base_delay * (2 ** 0) = 0.1
mock_sleep.assert_awaited_with(0.1)
async def test_non_network_error_raises_immediately(
self, mock_bot, mock_dispatcher
):
"""При не-сетевой ошибке исключение пробрасывается без повторов."""
mock_dispatcher.start_polling = AsyncMock(side_effect=ValueError("critical"))
with pytest.raises(ValueError, match="critical"):
await start_bot_with_retry(mock_bot, mock_dispatcher, max_retries=3)
mock_dispatcher.start_polling.assert_awaited_once()
@patch("helper_bot.main.asyncio.sleep", new_callable=AsyncMock)
async def test_max_retries_exceeded_raises(
self, mock_sleep, mock_bot, mock_dispatcher
):
"""При исчерпании попыток из-за сетевых ошибок исключение пробрасывается."""
mock_dispatcher.start_polling = AsyncMock(
side_effect=ConnectionError("network error")
)
with pytest.raises(ConnectionError, match="network error"):
await start_bot_with_retry(
mock_bot, mock_dispatcher, max_retries=2, base_delay=0.01
)
assert mock_dispatcher.start_polling.await_count == 2
assert mock_sleep.await_count == 1
async def test_timeout_error_triggers_retry(self, mock_bot, mock_dispatcher):
"""Ошибка с 'timeout' в сообщении считается сетевой и даёт повтор."""
call_count = 0
async def polling(*args, **kwargs):
nonlocal call_count
call_count += 1
if call_count == 1:
raise TimeoutError("timeout while connecting")
return None
mock_dispatcher.start_polling = AsyncMock(side_effect=polling)
with patch("helper_bot.main.asyncio.sleep", new_callable=AsyncMock):
await start_bot_with_retry(
mock_bot, mock_dispatcher, max_retries=3, base_delay=0.01
)
assert call_count == 2
@pytest.mark.unit
@pytest.mark.asyncio
class TestStartBot:
"""Тесты для start_bot с моками Bot, Dispatcher, start_metrics_server и т.д."""
@pytest.fixture
def mock_bdf(self, test_settings):
"""Мок фабрики зависимостей (bdf) с настройками и scoring_manager."""
bdf = MagicMock()
bdf.settings = {
**test_settings,
"Metrics": {"host": "127.0.0.1", "port": 9090},
}
scoring_manager = MagicMock()
scoring_manager.close = AsyncMock()
bdf.get_scoring_manager = MagicMock(return_value=scoring_manager)
return bdf
@patch("helper_bot.main.stop_metrics_server", new_callable=AsyncMock)
@patch("helper_bot.main.start_bot_with_retry", new_callable=AsyncMock)
@patch("helper_bot.main.start_metrics_server", new_callable=AsyncMock)
@patch("helper_bot.main.VoiceHandlers")
@patch("helper_bot.main.Dispatcher")
@patch("helper_bot.main.Bot")
async def test_start_bot_calls_metrics_server_and_polling(
self,
mock_bot_cls,
mock_dp_cls,
mock_voice_handlers_cls,
mock_start_metrics,
mock_start_retry,
mock_stop_metrics,
mock_bdf,
):
"""start_bot создаёт Bot и Dispatcher, запускает метрики, delete_webhook, start_bot_with_retry; в finally — stop_metrics и закрытие ресурсов."""
mock_bot = MagicMock()
mock_bot.delete_webhook = AsyncMock()
mock_bot.session = MagicMock()
mock_bot.session.close = AsyncMock()
mock_bot_cls.return_value = mock_bot
mock_dp = MagicMock()
mock_dp.update = MagicMock()
mock_dp.update.outer_middleware = MagicMock(return_value=None)
mock_dp.include_routers = MagicMock()
mock_dp.shutdown = MagicMock(return_value=lambda f: None)
mock_dp_cls.return_value = mock_dp
mock_voice_router = MagicMock()
mock_voice_handlers_cls.return_value.router = mock_voice_router
result = await start_bot(mock_bdf)
mock_bot_cls.assert_called_once()
mock_dp_cls.assert_called_once()
mock_bot.delete_webhook.assert_awaited_once_with(drop_pending_updates=True)
mock_start_metrics.assert_awaited_once_with("127.0.0.1", 9090)
mock_start_retry.assert_awaited_once()
mock_stop_metrics.assert_awaited_once()
mock_bdf.get_scoring_manager.return_value.close.assert_awaited()
mock_bot.session.close.assert_awaited()
assert result is mock_bot
@patch("helper_bot.main.stop_metrics_server", new_callable=AsyncMock)
@patch("helper_bot.main.start_bot_with_retry", new_callable=AsyncMock)
@patch("helper_bot.main.start_metrics_server", new_callable=AsyncMock)
@patch("helper_bot.main.VoiceHandlers")
@patch("helper_bot.main.Dispatcher")
@patch("helper_bot.main.Bot")
async def test_start_bot_uses_default_metrics_host_port_when_not_in_settings(
self,
mock_bot_cls,
mock_dp_cls,
mock_voice_handlers_cls,
mock_start_metrics,
mock_start_retry,
mock_stop_metrics,
mock_bdf,
test_settings,
):
"""Если в настройках нет Metrics, используются host 0.0.0.0 и port 8080."""
mock_bdf.settings = test_settings
mock_bot = MagicMock()
mock_bot.delete_webhook = AsyncMock()
mock_bot.session = MagicMock()
mock_bot.session.close = AsyncMock()
mock_bot_cls.return_value = mock_bot
mock_dp = MagicMock()
mock_dp.update = MagicMock()
mock_dp.update.outer_middleware = MagicMock(return_value=None)
mock_dp.include_routers = MagicMock()
mock_dp.shutdown = MagicMock(return_value=lambda f: None)
mock_dp_cls.return_value = mock_dp
mock_voice_handlers_cls.return_value.router = MagicMock()
await start_bot(mock_bdf)
mock_start_metrics.assert_awaited_once_with("0.0.0.0", 8080)