Black-box testing for aiogram 3.x Telegram bots. No network, no token, no mock servers - each test runs in milliseconds.
Inspired by Rust's teremock: send mock updates through your real dispatcher, then assert on what the bot sent back. Named after the folk tale about a little house whose guests arrive one by one - just like updates entering your bot.
pip install teremok
pip install teremok pytest-asyncio
teremok's tests (and the mock_bot fixture) are async def. Without
pytest-asyncio configured, async tests fail to run - add
this to your pyproject.toml (or the equivalent in pytest.ini):
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"The quickstart below depends on this setting.
from aiogram import Router
from aiogram.filters import Command
from aiogram.types import Message
from teremok import MockBot, MockMessageText
router = Router()
@router.message(Command("start"))
async def cmd_start(message: Message) -> None:
await message.answer("Hello!")
async def test_start(mock_bot): # `mock_bot` fixture ships with the package
bot = mock_bot(router)
result = await bot.dispatch(MockMessageText("/start"))
assert result.handled
assert bot.requests.send_message[0].text == "Hello!"Captures are typed aiogram objects, not JSON: bot.requests.send_message
returns SendMessage instances, bot.requests.send_photo[0].photo is the
actual InputFile your handler passed.
bot = mock_bot(form_router)
await bot.dispatch(MockMessageText("/form"))
await bot.dispatch(MockMessageText("Alice"))
await bot.dispatch(MockMessageText("30"))
assert bot.requests.send_message[-1].text == "Nice to meet you, Alice (30)!"
assert await bot.get_state() is None
# Or jump straight into the middle of a flow:
await bot.set_state(Form.age)
await bot.set_data({"name": "Bob"})bot.add_file("photo_1", b"...jpeg bytes...") # bot.download() returns this
await bot.dispatch(MockMessagePhoto(file_id="photo_1", caption="lunch"))
assert bot.requests.send_photo[0].photo.filename == "echo.jpg"from aiogram.methods import SendMessage
bot.add_result(SendMessage, ok=False, error_code=400,
description="Bad Request: chat not found")
# strict mode: MockBot(router, strict=True) fails on any un-queued callResults are keyed by method type: queuing a SendMessage result never answers an
AnswerCallbackQuery (or any other method) call that happens to run first - every
other call keeps auto-responding.
Every outgoing call is checked against documented Bot API rules before it
gets a response: text/caption length limits, HTML/MarkdownV2/legacy-Markdown
well-formedness, entity offset bounds, and inline keyboard button shape
(exactly one action field, callback_data ≤64 bytes). A violation raises a
genuine TelegramBadRequest - same description text, same check_response
route as a real rejection, nothing new to catch. Escape hatch for tests that
intentionally send malformed payloads: MockBot(router, validate=False).
Validation runs before queued results are consulted, so a call that fails
validation never consumes a queued result.
Full rule-by-rule reference, including what's deliberately not enforced, in
docs/validation.md.
Every Bot API method is captured (interception happens below all methods, at aiogram's session seam). Auto-response fidelity per method is tracked honestly in docs/coverage.md; known edge cases live in docs/quirks.md.
CI's freshness gate regenerates that table against the latest aiogram release on every run, so a new aiogram release can turn CI red until someone regenerates and commits the table - that's expected behavior, not a teremok bug.
Four runnable bots with full test suites — the tests are the best
documentation of how to use teremok. Start with the
examples guide; the showcase is the
pizza order wizard (inline-keyboard toggles,
FSM, gettext i18n) and its 18 tests.
Every example also runs live: BOT_TOKEN=... python -m examples.pizza_order_bot.
Tag vX.Y.Z and push - GitHub Actions builds and publishes via PyPI Trusted
Publishing (configure once in PyPI project settings).
The name and the whole idea are borrowed with love from teremock by @zerosixty — a Rust testing library for teloxide bots (MIT). teremok is its independent aiogram counterpart: no code is shared (different language, different framework), but the black-box testing philosophy — and the pun — are theirs. Related prior art: teloxide_tests.
MIT