Документация Тестирование ботов

Тестирование ботов

Тестирование ботов проверяет чат-бота единственным способом, доказывающим его работу — как диалог. Вы описываете диалог как упорядоченные ходы (стимул пользователя и ответ, который бот должен дать), прогоняете его против мок Bot API и получаете транскрипт с вердиктом на каждый ход.

Одним запросом бота не принять: корректность живёт в обмене — команда → ответ, состояние между ходами, круг с инлайн-кнопками. Тестирование ботов прогоняет ровно это.

Сценарий — список ходов

У каждого хода:

  • стимул — что делает пользователь: message (текст), command (/команда), callback (нажатие инлайн-кнопки), url (перейти по ссылке URL-кнопки) или first_button (взять первый вариант, который бот только что предложил);
  • ожидание — что бот должен сделать в ответ в течение окна: send_message, send_keyboard, edit_message, answer_callback, url_open (ссылка хода url открылась) или none (негативная проверка — бот должен молчать). Текст сверяется через textContains / textRegex; клавиатуры — через buttonContains.

Ход проходит, когда ожидание выполнено внутри окна ответа; запуск проходит, когда прошли все ходы.

Нажать кнопку, не зная её подписи

Кнопочные боты подписывают кнопки по-своему в каждом продукте, и подписи меняются вместе с текстами. Вписанная в сценарий подпись ломается на первой же правке формулировки.

first_button берёт первую кнопку той клавиатуры, которую бот показал на предыдущем ходе, — какой бы клавиатурой он ни воспользовался: инлайн-кнопка нажимается своим callback-значением, URL-кнопка — переходом по её ссылке, кнопка reply-клавиатуры — отправкой её подписи обратно. Подпись писать не нужно:

{"turns": [
  {"stimulus": {"kind": "command", "text": "/start"},
   "expectation": {"kind": "send_keyboard"}},
  {"stimulus": {"kind": "first_button"},
   "expectation": {"kind": "send_message", "textContains": "дат"}}
]}

Если на этом шаге бот не предложил ни одной кнопки, ход падает и прямо это сообщает — это настоящая находка про сценарий, потому что ровно здесь человек и застревает.

Нажать URL-кнопку

Бот может предложить вариант, который открывает ссылку, а не адресует бота ({"text": "Документация", "url": "…"} на инлайн-клавиатуре). Стимул url переходит по этой ссылке так, как её открывает чат-клиент, и результат перехода сам и есть вердикт хода:

{"turns": [
  {"stimulus": {"kind": "command", "text": "/start"},
   "expectation": {"kind": "send_keyboard"}},
  {"stimulus": {"kind": "first_button"},
   "expectation": {"kind": "url_open"}}
]}

Ход проходит, когда ссылка отвечает успешно (в транскрипте записывается действие url_open), и падает с причиной, если посадочная сломана (HTTP 500), недоступна или отвергнута. Ссылка на localhost или частный адрес по умолчанию отвергается — тем же исходящим гардом, что защищает все остальные исходящие вызовы, — если оператор не разрешил вызовы на частные адреса (ALLOW_PROXY_TO_PRIVATE_IPS=true); сообщение об отказе об этом говорит. Так вопрос «работает ли ещё ссылка бота» становится проверяемым шагом вместо ручной проверки.

В редакторе сценария выберите Пользователь → открывает кнопку-ссылку и вставьте ссылку либо нажимает первую предложенную кнопку, чтобы взять то, что показал бот; для нажатия ссылки поле Бот должен само выставляется в ссылка открывается успешно.

Мок Bot API

Тестирование ботов поднимает эмулированный сервер Telegram Bot-API. Направьте api_url вашего бота на выданную сессию (любая бот-библиотека поддерживает свой API URL — стандартный переключатель self-hosted bot-api), и бот ведёт себя точно как с настоящим сервером: long-poll getUpdates и вызовы sendMessage / editMessageText / answerCallbackQuery. Suite вкидывает стимулы как updates и читает вызовы бота как наблюдаемые действия хода.

memory — встроенный двойник для скриптовых сценариев без живого бота.

Тестирование ботов — многоплатформенное. Слой платформы — реестр, поэтому он не привязан к одному мессенджеру:

  • telegram-botapi — эмулированный Telegram Bot-API, описанный выше.
  • generic — нейтральный HTTP-протокол бота, на который может целиться любой фреймворк: ваш бот опрашивает GET <baseUrl>/updates за стимулами и POST-ит ответы на POST <baseUrl>/reply с {kind?, text, buttons?}. Для кастомного бота или мессенджера без выделенного адаптера.
  • memory — скриптовый двойник.

Новый конкретный мессенджер (VK Teams, Max, Slack…) — аддитивный адаптер, без изменений сценариев, API или UI.

Стенд бота: направить живого бота на фейковый Telegram

Стенд — это долгоживущий эмулированный сервер Bot API, которым вы управляете вручную: без сценария и тест-плана. Нужен, когда бот ещё пишется и с ним просто хочется поговорить.

  1. Специальное тестирование → Тестирование ботов → Стенд бота → Новый стенд.
  2. Скопируйте адрес Bot API и токен бота в конфигурацию своего бота (любая библиотека умеет свой адрес API — стандартный переключатель self-hosted Bot-API: base_url / api_url / custom API endpoint).
  3. Запустите бота. Он работает точно как с настоящим сервером: long-poll getUpdates либо регистрация вебхука через setWebhook.
  4. Отправьте апдейт — сообщение, /команду или нажатие кнопки — и всё, что сделал бот, появится в блоке Что сделал бот. Предложенные ботом кнопки кликабельны: один клик отправляет корректное нажатие (callback-данные для inline-кнопки, текст — для reply-клавиатуры).

Стенд живёт по таймеру простоя (30 минут по умолчанию, до 24 часов через ttlSeconds) и принадлежит своему пространству имён — из другого его не видно и им нельзя управлять.

Боты на вебхуках

Боты, получающие апдейты вебхуком, тестируются так же и без изменений в коде.

  • Если бот сам вызывает setWebhook, больше ничего не нужно: стенд запоминает адрес и начинает POST-ить апдейты туда.
  • Если вебхук настраивается вне бота — задайте его со стенда: Вебхук → Адрес вебхука (плюс необязательный секретный токен) или POST /bot-suite/sessions/{id}/webhook.

Каждая доставка — настоящий HTTP POST вашему боту с JSON Update и заголовком X-Telegram-Bot-Api-Secret-Token, так что бот, проверяющий секрет, продолжает его проверять. Стенд показывает каждую доставку с кодом ответа, числом попыток и ошибкой — «апдейт не дошёл» видно, а не угадывается. Бот может ответить на сам запрос вебхука вызовом метода ({"method": "sendMessage", ...}) — такой ответ записывается как действие бота наравне с прямым вызовом API.

Polling и вебхук взаимно исключают друг друга, ровно как на настоящем сервере: пока вебхук зарегистрирован, getUpdates отвечает 409 Conflict; deleteWebhook возвращает стенд к long-polling.

Бот на приватном адресе. Бот на localhost или во внутренней сети контейнеров доступен, только если оператор разрешил исходящие вызовы на приватные адреса (ALLOW_PROXY_TO_PRIVATE_IPS=true). Без этого адрес вебхука отклоняется сразу и с понятным сообщением — исходящие запросы во внутренние сети запрещены по умолчанию. Адреса cloud-metadata заблокированы в любой конфигурации.

Поддержанные методы Bot API

Стенд отвечает на любой метод: перечисленные ниже он понимает (они становятся проверяемыми действиями с текстом, подписью и клавиатурой), а любой другой отвечает ok: true и записывается как обобщённый api_call — неподдержанный метод никогда не ломает вашего бота.

Область Методы
Идентификация getMe, getChat
Получение апдейтов getUpdates (long-poll, offset / limit / allowed_updates), setWebhook, deleteWebhook, getWebhookInfo
Сообщения sendMessage, editMessageText, editMessageCaption, editMessageReplyMarkup, deleteMessage
Медиа sendPhoto, sendDocument, sendVideo, sendAudio, sendVoice, sendAnimation, sendSticker, sendVideoNote, sendMediaGroup
Взаимодействие answerCallbackQuery, answerInlineQuery, sendChatAction
Файлы getFile и скачивание файла по возвращённому file_path
Настройка setMyCommands, getMyCommands, deleteMyCommands, setChatMenuButton, setMyDescription, setMyName, logOut, close

Читаются оба вида клавиатур: inline_keyboard (нажимается callback-данными) и keyboard (нажимается отправкой текста кнопки). Запросы принимаются как JSON, form-encoded, query-строка или multipart-загрузка — в том виде, в каком их шлёт ваша библиотека.

Словарь ожиданий тот же: send_message, send_keyboard, edit_message, answer_callback, send_photo, send_document, send_media, send_chat_action, answer_inline_query, delete_message, api_call, url_open (ссылка хода url открылась) и none для проверки молчания.

В интерфейсе

Тестирование ботов в боковом меню, две вкладки.

Сценарии — центральная панель это чат: каждый ход показывает пузырь пользователя и пузырь ожидаемого ответа бота; кликните ход, чтобы отредактировать стимул и ожидание справа. Запустить размечает каждый пузырь вердиктом и задержкой.

Стенд бота — живой стенд, описанный выше: создать, скопировать адрес и токен в бота, отправлять апдейты, смотреть, что сделал бот, и управлять вебхуком.

Через API

Разовый сценарий одним вызовом. Для прогона telegram-botapi или generic обязателен sessionId — стенд, созданный выше, на который направлен бот. Без него прогон отклоняется с offendingField: sessionId и подсказкой, называющей эндпоинт стенда, — вместо минта стенда, к которому никто не подключён (обычная причина «все ходы провалены»). platform: "memory" — сценарный дублёр, стенд ему не нужен.

Стенд generic видит в качестве ответов бота только send_message и send_keyboard — edit_message и answer_callback специфичны для Telegram. bot_stand_create принимает platform: "generic" для таких ботов (по умолчанию — эмуляция Telegram).

Разовый сценарий одним вызовом:

curl -X POST "http://localhost:5770/api/v1/namespaces/my-namespace/bot-suite/scenarios/run" \
  -H "X-API-Key: $TOKEN" -H "Content-Type: application/json" -d '{
  "platform": "telegram-botapi",
  "scenario": {"name": "order flow", "turns": [
    {"stimulus": {"kind": "command", "text": "/start"},
     "expectation": {"kind": "send_message", "textContains": "Welcome", "buttonContains": "Order"}},
    {"stimulus": {"kind": "callback", "data": "order"},
     "expectation": {"kind": "send_message", "textRegex": "Order created #\\d+"}}
  ]}
}'

Сохранённые сценарии: POST /bot-suite/scenarios, GET /bot-suite/scenarios, POST /bot-suite/scenarios/{id}/run, GET /bot-suite/scenarios/{id}/runs.

Эндпоинты стенда (все — в рамках пространства имён):

Вызов Что делает
POST /bot-suite/sessions создать стенд — возвращает sessionId, token, baseUrlAbsolute (необязательный ttlSeconds)
GET /bot-suite/sessions список живых стендов с режимом и временем истечения
GET /bot-suite/sessions/{id} один стенд: режим, состояние вебхука, последние доставки
POST /bot-suite/sessions/{id}/updates отправить апдейт — {kind, text, data, chatId}; в режиме вебхука ответ сообщает, дошёл ли он до бота
GET /bot-suite/sessions/{id}/actions что сделал бот, плюс лог доставок
POST /bot-suite/sessions/{id}/webhook зарегистрировать вебхук бота ({url, secretToken, allowedUpdates, maxConnections})
DELETE /bot-suite/sessions/{id}/webhook вернуться к long-polling
DELETE /bot-suite/sessions/{id} закрыть стенд
# Создать стенд и поговорить с ботом, направленным на него
curl -X POST "http://localhost:5770/api/v1/namespaces/my-namespace/bot-suite/sessions" \
  -H "X-API-Key: $TOKEN" -H "Content-Type: application/json" -d '{"ttlSeconds": 3600}'
# → {"sessionId":"...","token":"...","baseUrlAbsolute":"http://localhost:5770/botapi/..."}

curl -X POST "http://localhost:5770/api/v1/namespaces/my-namespace/bot-suite/sessions/$ID/updates" \
  -H "X-API-Key: $TOKEN" -H "Content-Type: application/json" -d '{"kind": "command", "text": "/start"}'

curl "http://localhost:5770/api/v1/namespaces/my-namespace/bot-suite/sessions/$ID/actions" \
  -H "X-API-Key: $TOKEN"

Из AI-агента

MCP-инструмент bot_dialog_run прогоняет разовый сценарий и возвращает транскрипт одним вызовом; bot_scenario_create / bot_scenario_run_saved / bot_dialog_get_run управляют сохранёнными сценариями.

Для стенда: bot_stand_create возвращает адрес и токен для конфигурации бота, bot_stand_send_update действует как пользователь, bot_stand_actions читает, что сделал бот, bot_stand_set_webhook переключает доставку на вебхук, а bot_stand_list / bot_stand_get / bot_stand_close управляют стендами. Это полный цикл тестирования бота без человека.

В тест-плане

Добавьте элемент bot_scenario со ссылкой на сохранённый сценарий — шаг проходит, когда диалог прошёл, и падает (с транскриптом в отчёте шага) иначе.

Если сценарий ожидает ответа бота (любое ожидание, кроме тишины), элемент обязан назвать стенд, который бот опрашивает: укажите {"sessionId": "<id стенда>"} в parameters элемента (id стенда из Bot Suite или bot_stand_list) и держите api_url бота направленным на этот стенд. Без этого элемент отклоняется с понятным сообщением — вместо прогона на стенде, к которому никто не подключён, где падал бы каждый ход. Сценарий только из проверок тишины работает без стенда.

Как шаг тест-кейса (накопленный регресс)

У бота нет ручки, поэтому обычный шаг кейса не может сказать о нём ничего. Шаг типа bot прогоняет диалог — через тот же движок разговорных сценариев. Именно это делает набор тест-кейсов разговорного продукта исполнимым, и именно это позволяет перепрогонять накопленные кейсы без человека.

{
  "executorType": "bot",
  "executorConfig": {
    "platform": "telegram-botapi",
    "sessionId": "<стенд, на который указывает api_url бота>",
    "scenario": {"name": "оформление", "turns": [
      {"stimulus": {"kind": "command", "text": "/start"},
       "expectation": {"kind": "send_message", "textContains": "Здравствуйте"}}
    ]},
    "successAny": ["заказ оформлен"]
  }
}
  • sessionId обязателен для telegram-botapi и generic, и он не создаётся за вас: бот, которого проверяем, уже должен опрашивать этот стенд (свой api_url). Шаг без него падает с именованной ошибкой, а не идёт на пустой стенд — там падал бы каждый ход, а в отчёте это выглядело бы дефектом вашего продукта.
  • scenario — диалог ровно в той форме, которую принимает bot_dialog_run (см. «Сценарий — это список ходов» выше). Вместо него шаг может назвать сохранённый сценарий ("scenarioId": "<id из «Сценарии»>") — платформа тогда берётся из сохранённой записи, а прогон появляется в её собственной истории.
  • successAny необязателен и задаёт критерий для всего диалога: шаг падает, если последняя реплика бота не содержит ни одной из этих строк. Поток, где прошли все ходы, а разговор кончился не там, — ровно этот случай.
  • platform по умолчанию telegram-botapi. memory прогоняет сценарный дубль и стенда не требует.

Вердикт шага — вердикт диалога: pass, когда поток прошёл (и, если объявлена, цель), иначе fail, с пошаговым транскриптом в отчёте шага. Больше в кейсе менять нечего — шаг bot исполняется как любой автоматический шаг.

Ограничения — что это и чем оно не является

Стенд — это мок Bot API, а не Telegram. Границы важно понимать:

  • Настоящего Telegram нет. Реальным пользователям ничего не уходит, номер телефона и регистрация бота не нужны, а «отправленные» ботом сообщения попадают только в лог действий стенда. Загруженные на стенд файлы держатся в памяти (несколько мегабайт, самые свежие) и исчезают вместе со стендом.
  • Только перечисленные виды апдейтов. Пользователь моделируется сообщением, /командой, нажатием inline-кнопки, переходом по ссылке или inline-запросом. События групп и каналов, платежи, опросы, отредактированные сообщения и изменения участников чата не моделируются (бот, вызывающий такие методы, всё равно получает ok: true, а вызов записывается).
  • Доставка вебхука ограничена и не вечна. Неудачная доставка повторяется несколько раз с паузами, затем от неё отказываются с записью причины — настоящий сервис повторяет намного дольше. Последняя ошибка видна в getWebhookInfo.
  • Стенд живёт на том узле, где создан. Если админ-узлов несколько за балансировщиком — направляйте бота на адрес этого узла: стенд не общий для кластера и не переживает перезапуск.
  • Стенд истекает по простою — 30 минут по умолчанию, максимум 24 часа.
  • Прогон диалога ограничен: окно ответа каждого хода не более 5 минут, интервал опроса не менее 100 мс, число ходов ограничено — прогон всегда завершается.