From 03454eb1baf866116853d900413f23e4cb9261a0 Mon Sep 17 00:00:00 2001 From: Vlad Date: Sat, 4 Oct 2025 18:20:53 +0500 Subject: [PATCH 1/8] Docs: add testing guide + newsfragment 1727.docs.rst --- docs/guide/testing.rst | 88 +++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + newsfragments/.docs.rst | 8 ++++ 3 files changed, 97 insertions(+) create mode 100644 docs/guide/testing.rst create mode 100644 newsfragments/.docs.rst diff --git a/docs/guide/testing.rst b/docs/guide/testing.rst new file mode 100644 index 00000000..2f33f341 --- /dev/null +++ b/docs/guide/testing.rst @@ -0,0 +1,88 @@ +Testing your bot (pytest) +========================= + +This guide shows how to test your handlers **without using the real Telegram API**. +We will use `pytest` and `pytest-asyncio`. + +Installation:: + + pip install -U pytest pytest-asyncio + +Simple echo handler +------------------- + +**Handler:** + +.. code-block:: python + + # app/bot.py + from aiogram.types import Message + + async def echo_handler(message: Message): + await message.answer(message.text) + +**Test:** + +.. code-block:: python + + # tests/test_echo.py + import pytest + from app.bot import echo_handler + + @pytest.mark.asyncio + async def test_echo_handler(): + sent = [] + + class DummyMessage: + def __init__(self, text): + self.text = text + async def answer(self, text): + sent.append(text) + + msg = DummyMessage("hello") + await echo_handler(msg) + + assert sent == ["hello"] + +Callback query example +---------------------- + +**Handler:** + +.. code-block:: python + + # app/callbacks.py + from aiogram.types import CallbackQuery + + async def ping_pong(cb: CallbackQuery): + if cb.data == "ping": + await cb.message.edit_text("pong") + await cb.answer() + +**Test:** + +.. code-block:: python + + # tests/test_callbacks.py + import pytest + from app.callbacks import ping_pong + + @pytest.mark.asyncio + async def test_ping_pong(): + calls = {"edited": None, "answered": False} + + class DummyMsg: + async def edit_text(self, text): + calls["edited"] = text + + class DummyCb: + data = "ping" + message = DummyMsg() + async def answer(self): + calls["answered"] = True + + cb = DummyCb() + await ping_pong(cb) + + assert calls["edited"] == "pong" + assert calls["answered"] is True diff --git a/docs/index.rst b/docs/index.rst index a923cab1..e60361c1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -27,3 +27,4 @@ Contents utils/index changelog contributing + guide/testing diff --git a/newsfragments/.docs.rst b/newsfragments/.docs.rst new file mode 100644 index 00000000..7e81977c --- /dev/null +++ b/newsfragments/.docs.rst @@ -0,0 +1,8 @@ +Docs: add guide "Testing your bot" (pytest examples) + +Add a new documentation page that shows how to test bot handlers with pytest +without sending real requests to the Telegram API. The guide includes: +- an echo handler example +- a callback query example + +Both examples use dummy objects so developers can test logic in isolation. From 6e777524bc3c51ea0f9dd5dfedcd72ca543c9dcc Mon Sep 17 00:00:00 2001 From: Vlad Date: Sat, 4 Oct 2025 18:27:12 +0500 Subject: [PATCH 2/8] changelog: add 1728.docs.rst --- newsfragments/{.docs.rst => 1728.docs.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename newsfragments/{.docs.rst => 1728.docs.rst} (100%) diff --git a/newsfragments/.docs.rst b/newsfragments/1728.docs.rst similarity index 100% rename from newsfragments/.docs.rst rename to newsfragments/1728.docs.rst From 6cf0ed61f7362c3b446b9f2a314d7d91ab83b256 Mon Sep 17 00:00:00 2001 From: Vlad Date: Sat, 4 Oct 2025 18:30:40 +0500 Subject: [PATCH 3/8] changelog: add 1728.docs.rst --- newsfragments/1728.docs.rst | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 newsfragments/1728.docs.rst diff --git a/newsfragments/1728.docs.rst b/newsfragments/1728.docs.rst deleted file mode 100644 index 7e81977c..00000000 --- a/newsfragments/1728.docs.rst +++ /dev/null @@ -1,8 +0,0 @@ -Docs: add guide "Testing your bot" (pytest examples) - -Add a new documentation page that shows how to test bot handlers with pytest -without sending real requests to the Telegram API. The guide includes: -- an echo handler example -- a callback query example - -Both examples use dummy objects so developers can test logic in isolation. From f9ba31adde039cf205c98babf9fc7b070f083e6f Mon Sep 17 00:00:00 2001 From: Vlad Date: Sat, 4 Oct 2025 18:31:58 +0500 Subject: [PATCH 4/8] changelog: add CHANGES/1728.doc.rst --- CHANGES/1728.doc.rst | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 CHANGES/1728.doc.rst diff --git a/CHANGES/1728.doc.rst b/CHANGES/1728.doc.rst new file mode 100644 index 00000000..ab790e34 --- /dev/null +++ b/CHANGES/1728.doc.rst @@ -0,0 +1,7 @@ +Docs: add guide "Testing your bot" (pytest examples) + +A new documentation page explaining how to test bot handlers with pytest +without sending real requests to Telegram API. +Includes: +- an echo handler example +- a callback query example From 84ba1a9582eff8e00f7b3da4fe127afc1f735b70 Mon Sep 17 00:00:00 2001 From: Vlad Date: Sat, 4 Oct 2025 18:39:11 +0500 Subject: [PATCH 5/8] Fix CHANGES fragment filename for PR #1728 --- CHANGES/{1728.doc.rst => 1728.docs.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename CHANGES/{1728.doc.rst => 1728.docs.rst} (100%) diff --git a/CHANGES/1728.doc.rst b/CHANGES/1728.docs.rst similarity index 100% rename from CHANGES/1728.doc.rst rename to CHANGES/1728.docs.rst From bd269d05be787b0a13f7f43e46d0706ab99ed7b4 Mon Sep 17 00:00:00 2001 From: Vlad Date: Sat, 4 Oct 2025 22:41:27 +0500 Subject: [PATCH 6/8] Docs: rename doc. file --- CHANGES/1728.doc.rst | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 CHANGES/1728.doc.rst diff --git a/CHANGES/1728.doc.rst b/CHANGES/1728.doc.rst new file mode 100644 index 00000000..ab790e34 --- /dev/null +++ b/CHANGES/1728.doc.rst @@ -0,0 +1,7 @@ +Docs: add guide "Testing your bot" (pytest examples) + +A new documentation page explaining how to test bot handlers with pytest +without sending real requests to Telegram API. +Includes: +- an echo handler example +- a callback query example From 5da3eb41f4569a68a332061aff83deee024bd8a6 Mon Sep 17 00:00:00 2001 From: Vlad Date: Sat, 4 Oct 2025 22:44:20 +0500 Subject: [PATCH 7/8] Remove invalid newsfragment 1728.docs.rst --- CHANGES/1728.docs.rst | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 CHANGES/1728.docs.rst diff --git a/CHANGES/1728.docs.rst b/CHANGES/1728.docs.rst deleted file mode 100644 index ab790e34..00000000 --- a/CHANGES/1728.docs.rst +++ /dev/null @@ -1,7 +0,0 @@ -Docs: add guide "Testing your bot" (pytest examples) - -A new documentation page explaining how to test bot handlers with pytest -without sending real requests to Telegram API. -Includes: -- an echo handler example -- a callback query example From 34ded4312249e2008974816a0c632fa6c7ed6b47 Mon Sep 17 00:00:00 2001 From: Vlad Date: Tue, 7 Oct 2025 19:03:40 +0500 Subject: [PATCH 8/8] Docs: polish examples, and see also section --- docs/guide/testing.rst | 116 ++++++++++++++++++----------------------- 1 file changed, 50 insertions(+), 66 deletions(-) diff --git a/docs/guide/testing.rst b/docs/guide/testing.rst index 2f33f341..1d916094 100644 --- a/docs/guide/testing.rst +++ b/docs/guide/testing.rst @@ -4,85 +4,69 @@ Testing your bot (pytest) This guide shows how to test your handlers **without using the real Telegram API**. We will use `pytest` and `pytest-asyncio`. -Installation:: +Configure pytest once (no need to mark every test): - pip install -U pytest pytest-asyncio +.. code-block:: toml -Simple echo handler -------------------- + # pyproject.toml + [tool.pytest.ini_options] + asyncio_mode = "auto" -**Handler:** + +Example: testing a simple handler: .. code-block:: python - # app/bot.py + from unittest.mock import AsyncMock + from aiogram import Router, F from aiogram.types import Message - async def echo_handler(message: Message): - await message.answer(message.text) + router = Router() -**Test:** + @router.message(F.text == "/start") + async def start(message: Message): + await message.answer("Hello!") + + async def test_start_handler(): + # Bot and message stubs + bot = AsyncMock() + msg = Message( + message_id=1, + date=None, + chat={"id": 1, "type": "private"}, + text="/start", + ) + + # Emulate answer() call + message = AsyncMock(spec=Message) + message.answer = AsyncMock() + + # Run handler + await start(message) + + # Assert: answer called with expected payload + message.answer.assert_awaited_once_with("Hello!") + + +Mocking Bot API +=============== + +To assert Bot API calls, patch the method and verify arguments: .. code-block:: python - # tests/test_echo.py - import pytest - from app.bot import echo_handler + from unittest.mock import AsyncMock, patch + from aiogram import Bot - @pytest.mark.asyncio - async def test_echo_handler(): - sent = [] + async def test_bot_send_message(): + bot = Bot("42:TEST", parse_mode=None) - class DummyMessage: - def __init__(self, text): - self.text = text - async def answer(self, text): - sent.append(text) + with patch.object(Bot, "send_message", new_callable=AsyncMock) as send_msg: + await bot.send_message(123, "ping") + send_msg.assert_awaited_once_with(123, "ping") - msg = DummyMessage("hello") - await echo_handler(msg) +See also +-------- - assert sent == ["hello"] - -Callback query example ----------------------- - -**Handler:** - -.. code-block:: python - - # app/callbacks.py - from aiogram.types import CallbackQuery - - async def ping_pong(cb: CallbackQuery): - if cb.data == "ping": - await cb.message.edit_text("pong") - await cb.answer() - -**Test:** - -.. code-block:: python - - # tests/test_callbacks.py - import pytest - from app.callbacks import ping_pong - - @pytest.mark.asyncio - async def test_ping_pong(): - calls = {"edited": None, "answered": False} - - class DummyMsg: - async def edit_text(self, text): - calls["edited"] = text - - class DummyCb: - data = "ping" - message = DummyMsg() - async def answer(self): - calls["answered"] = True - - cb = DummyCb() - await ping_pong(cb) - - assert calls["edited"] == "pong" - assert calls["answered"] is True +- :ref:`aiogram.utils.magic_filter` +- :ref:`pytest documentation `