Test your Telegram bots easily
Project description
aiogram_bot_tester
Navigation / Навигация - English version - Русская версия
A lightweight testing utility for offline testing of aiogram bots without real Telegram API calls.
Its goal is to make it easy to test bot logic deterministically by simulating Telegram updates, intercepting bot API calls, and exposing a clean assertion-friendly response layer.
Легковесная библиотека для оффлайн тестирования ботов, написанных на aiogram без использования интернета и Telegram API.
Цель данного пакета состоит в том, чтобы обеспечить проверку логики работы бота при помощи симуляции реальных Telegram-запросов с использованием чистых и удобных абстракций.
Installation / Установка
Stable version / Стабильная версия:
pip install aiogram_bot_tester
Latest development version / Последняя разрабатываемая версия
pip install git+https://github.com/samedit66/aiogram-bot-tester.git
Quick example / Пример
import aiogram
from aiogram import filters, types
from aiogram_bot_tester import BotTester
import pytest
router = aiogram.Router()
@router.message(filters.CommandStart())
async def cmd_start(message: types.Message) -> None:
await message.answer("Hello! What's your name?")
@pytest.mark.asyncio
async def test_it() -> None:
# Make a tester
tester = BotTester.from_routers(router)
# If you have a `Bot` instance with a `Dispatcher`, use the following:
# tester = BotTester(bot=bot, dispatcher=dispatcher)
# Send a command
response = await tester.send_message("/start")
# Check that ``"Hello"`` is in response
assert response.contains("Hello")
English Version
Core API
BotTester.from_routers(*routers)
Construct a BotTester instance from a sequence of Routerss.
tester = BotTester.from_routers(router)
send_message(text, **kwargs)
Send a message to the bot. Returns a special Response object described below.
response = await tester.send_message("/start")
assert response.text == "Hello"
send_command(command, *command_args, prefix="/")
Helper to make sending commands more easily.
Instead of
await tester.send_message(f"/sum {a} {b}")
you can simply write
await tester.send_command("sum", a, b)
click_reply_button(text, **kwargs)
Simulates clicking a reply button. Actually, it's just another way of calling send_message. Exists just to clarify intention.
response = await tester.click_reply_button("Option A")
click_inline_button(label)
Simulates clicking an inline button.
response = await tester.click_inline_button("Press")
Response object
Each interaction with a BotTester returns a Response object.
@dataclass
class Response:
text: str | None
state: State | None
message: object | None
Response.text
Last bot message text.
Response.state
FSM state.
Example:
class Form(StatesGroup):
name = State()
surname = State()
age = State()
# Inside a test:
response = await tester.send_message("/start")
assert response.state == Form.name # in_state() method does the same
Response.contains(substring)
Returns True if this response text contains the given substring, False otherwise.
Example:
assert response.contains("Hello")
Response.matches(regex)
Returns True if this response text matches the given regex, False otherwise.
Example:
assert response.matches("\d+")
Response.has_inline_button(label)
Returns True if this response has an inline button with label, False otherwise.
Example:
assert response.has_inline_button("Click me!")
Response.has_reply_button(label)
Returns True if this response has a button with label, False otherwise.
Example:
assert response.has_reply_button("Click me!")
Response.has_inline_keyboard_like(keyboard)
Returns True if this response has the specified inline keyboard, False otherwise.
Example:
assert response.has_inline_keyboard_like([
["Yes", "No"],
["Cancel"],
])
Response.has_reply_keyboard_like(keyboard)
Returns True if this response has the specified reply keyboard, False otherwise.
Example:
assert response.has_reply_keyboard_like([
["1", "2", "3"],
["4", "5", "6"],
])
Response.in_state(state)
Returns True if after this response the state is state, False otherwise.
Example:
assert response.in_state(Form.name)
🚀 Full example
import pytest
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram_bot_tester import BotTester
router = Router()
@router.message()
async def echo(message: Message):
await message.answer(
f"Echo: {message.text}",
reply_markup=InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="Press me", callback_data="press")]
]
),
)
@router.callback_query(F.data == "press")
async def on_press(callback: CallbackQuery):
await callback.message.answer("Button clicked!")
@pytest.mark.asyncio
async def test_full_flow():
tester = BotTester.from_routers(router)
response = await tester.send_message("Hello")
assert response.text == "Echo: Hello"
response = await tester.click_inline_button("Press me")
assert response.text == "Button clicked!"
Русская версия
Core API
BotTester.from_routers(*routers)
Создает объект BotTester из набора роутеров.
tester = BotTester.from_routers(router1, router2, router3)
send_message(text, **kwargs)
Симулирует отправку сообщения боту. Возвращает специальный объект Response, рассматриваемый далее.
response = await tester.send_message("/start")
assert response.text == "Привет"
send_command(command, *command_args, prefix="/")
Вспомогательная функция для более легкой отправки команд.
Вместо
await tester.send_message(f"/sum {a} {b}")
можно просто писать
await tester.send_command("sum", a, b)
click_reply_button(text, **kwargs)
Симулирует нажатие на reply-кнопку. На самом деле, это просто синоним для метода send_message, но с названием отражающим суть.
response = await tester.click_reply_button("Вариант А")
click_inline_button(label)
Симулирует нажатие на inline-кнопку.
response = await tester.click_inline_button("Нажми")
Response object
В результате любого взаимодействия с BotTester, возвращается объект-ответ Response.s
@dataclass
class Response:
text: str | None
state: State | None
message: object | None
Response.text
Последнее сообщение бота.
Response.state
Состояние.
Пример:
class Form(StatesGroup):
name = State()
surname = State()
age = State()
# Внутри теста:
response = await tester.send_message("/start")
assert response.state == Form.name # in_state() делает аналогичное действие
Response.contains(substring)
Возвращает True, если текст ответа содержит указанную подстроку, иначе False.
Пример:
assert response.contains("Привет")
Response.matches(regex)
Возвращает True, если текст ответа соответствует регулярному выражению, иначе False.
Пример:
assert response.matches("\d+")
Response.has_inline_button(label)
Возвращает True, если в ответе есть inline-кнопка с указанным текстом.
Привет:
assert response.has_inline_button("Нажми меня!")
Response.has_reply_button(label)
Возвращает True, если в ответе есть reply-кнопка с указанным текстом.
Example:
assert response.has_reply_button("Нажми меня!")
Response.has_inline_keyboard_like(keyboard)
Возвращает True, если inline-клавиатура совпадает с заданной структурой.
Example:
assert response.has_inline_keyboard_like([
["Да", "Нет"],
["Отмена"],
])
Response.has_reply_keyboard_like(keyboard)
Возвращает True, если reply-клавиатура совпадает с заданной структурой.
Example:
assert response.has_reply_keyboard_like([
["1", "2", "3"],
["4", "5", "6"],
])
Response.in_state(state)
Возвращает True, если бот находится в заданном состоянии.
Пример:
assert response.in_state(Form.name)
🚀 Полный пример теста
import pytest
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram_bot_tester import BotTester
router = Router()
@router.message()
async def echo(message: Message):
await message.answer(
f"Echo: {message.text}",
reply_markup=InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="Нажми меня!", callback_data="press")]
]
),
)
@router.callback_query(F.data == "press")
async def on_press(callback: CallbackQuery):
await callback.message.answer("Кнопка нажата!")
@pytest.mark.asyncio
async def test_full_flow():
tester = BotTester.from_routers(router)
response = await tester.send_message("Hello")
assert response.text == "Echo: Hello"
response = await tester.click_inline_button("Нажми меня!")
assert response.text == "Кнопка нажата!"
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aiogram_bot_tester-0.2.0.tar.gz.
File metadata
- Download URL: aiogram_bot_tester-0.2.0.tar.gz
- Upload date:
- Size: 6.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.12 {"installer":{"name":"uv","version":"0.11.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1dddb5887e6a68bfa8df8d1bb3c0b0c5867ff0b939f5bc7b8475ca89c415de20
|
|
| MD5 |
6afe22f2fc30c3bb297eaf59595c5865
|
|
| BLAKE2b-256 |
7ef37d0d6d7323e1e034dd888f45df4ceea3d44ac9cdc58a9e8907facb31d38c
|
File details
Details for the file aiogram_bot_tester-0.2.0-py3-none-any.whl.
File metadata
- Download URL: aiogram_bot_tester-0.2.0-py3-none-any.whl
- Upload date:
- Size: 7.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.12 {"installer":{"name":"uv","version":"0.11.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fb6242d0c7ac4abbc248c58df3c057d5ef6ed162be9c4fe1482b71f9df8b19ca
|
|
| MD5 |
4993d1ecfe99b2bf61d5c707f86eaff6
|
|
| BLAKE2b-256 |
528d76d40e151255ec3c3543d1fb09c65f02cbef901d21dfbe2672b8e0de832d
|