Тестирование диалогов¶
Пакет maxo.dialogs.test_tools позволяет тестировать диалоги без реального
Max API и без подключения к сети. Тесты выполняются быстро и детерминировано:
бот работает в памяти, а отправленные сообщения перехватываются для проверки.
Используйте этот подход на уровне интеграционных тестов: реальная бизнес-логика и опционально реальная БД, но без Telegram/Max.
Компоненты¶
JsonMemoryStorage¶
Реализация FSM-хранилища в памяти. Заменяет Redis в тестах - состояния диалогов хранятся в словаре и не требуют внешних зависимостей.
from maxo.dialogs.test_tools.memory_storage import JsonMemoryStorage
storage = JsonMemoryStorage()
BotClient¶
Симулятор пользователя. Позволяет отправлять текстовые сообщения и нажимать кнопки без реального подключения к API.
from maxo.dialogs.test_tools import BotClient
client = BotClient(dp, user_id=1, chat_id=1)
MockMessageManager¶
Перехватывает исходящие сообщения бота. Вместо отправки через API сохраняет их во внутренний список - вы можете прочитать их и проверить содержимое.
from maxo.dialogs.test_tools import MockMessageManager
message_manager = MockMessageManager()
Настройка окружения¶
Соберите Dispatcher с JsonMemoryStorage и передайте MockMessageManager
в setup_dialogs(). DefaultKeyBuilder(with_destiny=True) обязателен
для корректной работы диалогов.
from maxo import Dispatcher
from maxo.dialogs import setup_dialogs
from maxo.dialogs.test_tools import BotClient, MockMessageManager
from maxo.dialogs.test_tools.memory_storage import JsonMemoryStorage
from maxo.fsm.key_builder import DefaultKeyBuilder
from maxo.fsm.storages.memory import SimpleEventIsolation
storage = JsonMemoryStorage()
message_manager = MockMessageManager()
key_builder = DefaultKeyBuilder(with_destiny=True)
event_isolation = SimpleEventIsolation(key_builder=key_builder)
dp = Dispatcher(
storage=storage,
key_builder=key_builder,
events_isolation=event_isolation,
)
dp.include(my_dialog)
setup_dialogs(dp, message_manager=message_manager, events_isolation=event_isolation)
client = BotClient(dp)
Проверка рендеринга окна¶
Отправьте команду и прочитайте последнее сообщение из MockMessageManager.
Текст доступен через msg.body.text, кнопки - через msg.body.keyboard.buttons.
await client.send("/start")
msg = message_manager.last_message()
assert "Главное меню" in msg.body.text
# Проверка кнопок (фильтруем только CallbackButton)
from maxo.types import CallbackButton
buttons = [btn.text for row in msg.body.keyboard.buttons for btn in row if isinstance(btn, CallbackButton)]
assert "Подробнее" in buttons
Используйте reset_history() чтобы очистить историю между шагами теста -
так вы точно читаете сообщение от нужного действия:
await client.send("/start")
message_manager.reset_history() # очищаем - дальше только новые сообщения
await client.click(msg, locator)
detail_msg = message_manager.last_message()
Локаторы кнопок¶
BotClient.click() принимает локатор - объект, который находит нужную кнопку
в сообщении.
Проверка переходов¶
Нажатие кнопки возвращает callback_id. Передайте его в assert_answered()
чтобы убедиться, что бот ответил на callback (без ответа у пользователя
останется «часы» на кнопке).
await client.send("/start")
menu_msg = message_manager.last_message()
message_manager.reset_history()
callback_id = await client.click(menu_msg, InlineButtonTextLocator("Подробнее"))
message_manager.assert_answered(callback_id)
detail_msg = message_manager.last_message()
assert "Детальная страница" in detail_msg.body.text
Паттерны pytest¶
Выносите создание окружения в фикстуры pytest чтобы изолировать каждый тест.
Используйте autouse=True чтобы автоматически очищать историю.
import pytest
import pytest_asyncio
from maxo import Dispatcher
from maxo.dialogs import setup_dialogs
from maxo.dialogs.test_tools import BotClient, MockMessageManager
from maxo.dialogs.test_tools.memory_storage import JsonMemoryStorage
from maxo.fsm.key_builder import DefaultKeyBuilder
@pytest_asyncio.fixture(scope="session")
async def dp() -> Dispatcher:
storage = JsonMemoryStorage()
_dp = Dispatcher(
storage=storage,
key_builder=DefaultKeyBuilder(with_destiny=True),
)
_dp.include(my_dialog)
return _dp
@pytest_asyncio.fixture(scope="session")
async def message_manager(dp: Dispatcher) -> MockMessageManager:
mm = MockMessageManager()
setup_dialogs(dp, message_manager=mm)
return mm
@pytest_asyncio.fixture(scope="session")
async def client(dp: Dispatcher) -> BotClient:
return BotClient(dp)
@pytest.fixture(autouse=True)
def reset(message_manager: MockMessageManager):
message_manager.reset_history()
yield
message_manager.reset_history()
@pytest.mark.asyncio
async def test_main_menu(client: BotClient, message_manager: MockMessageManager):
await client.send("/start")
msg = message_manager.last_message()
assert "Главное меню" in msg.body.text
Полный пример¶
Рабочий пример с обоими сценариями (рендеринг окна и переход по кнопке):
examples/dialogs_testing.py