Тестирование ботов
Тестирование ботов проверяет чат-бота единственным способом, доказывающим его работу — как диалог. Вы описываете диалог как упорядоченные ходы (стимул пользователя и ответ, который бот должен дать), прогоняете его против мок 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, которым вы управляете вручную: без сценария и тест-плана. Нужен, когда бот ещё пишется и с ним просто хочется поговорить.
- Специальное тестирование → Тестирование ботов → Стенд бота → Новый стенд.
- Скопируйте адрес Bot API и токен бота в конфигурацию своего бота (любая библиотека умеет свой адрес API — стандартный переключатель self-hosted Bot-API:
base_url/api_url/ custom API endpoint). - Запустите бота. Он работает точно как с настоящим сервером: long-poll
getUpdatesлибо регистрация вебхука черезsetWebhook. - Отправьте апдейт — сообщение,
/командуили нажатие кнопки — и всё, что сделал бот, появится в блоке Что сделал бот. Предложенные ботом кнопки кликабельны: один клик отправляет корректное нажатие (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 мс, число ходов ограничено — прогон всегда завершается.