aiogram-testing 1.1.0__tar.gz → 1.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. aiogram_testing-1.2.0/PKG-INFO +163 -0
  2. aiogram_testing-1.2.0/README.md +140 -0
  3. aiogram_testing-1.2.0/aiogram_tests/__init__.py +6 -0
  4. aiogram_testing-1.2.0/aiogram_tests/exceptions.py +6 -0
  5. aiogram_testing-1.2.0/aiogram_tests/handler/__init__.py +39 -0
  6. {aiogram_testing-1.1.0 → aiogram_testing-1.2.0}/aiogram_tests/handler/base.py +4 -7
  7. aiogram_testing-1.2.0/aiogram_tests/handler/handler.py +178 -0
  8. {aiogram_testing-1.1.0 → aiogram_testing-1.2.0}/aiogram_tests/mocked_bot.py +9 -1
  9. aiogram_testing-1.2.0/aiogram_tests/pytest_plugin.py +36 -0
  10. aiogram_testing-1.2.0/aiogram_tests/requester.py +130 -0
  11. aiogram_testing-1.2.0/aiogram_tests/tester.py +175 -0
  12. aiogram_testing-1.2.0/aiogram_tests/types/dataset/__init__.py +155 -0
  13. aiogram_testing-1.2.0/aiogram_tests/types/dataset/base.py +76 -0
  14. aiogram_testing-1.2.0/aiogram_tests/types/dataset/chats.py +109 -0
  15. aiogram_testing-1.2.0/aiogram_tests/types/dataset/entities.py +68 -0
  16. aiogram_testing-1.2.0/aiogram_tests/types/dataset/events.py +78 -0
  17. aiogram_testing-1.2.0/aiogram_tests/types/dataset/media.py +143 -0
  18. aiogram_testing-1.2.0/aiogram_tests/types/dataset/messages.py +329 -0
  19. aiogram_testing-1.2.0/aiogram_tests/types/dataset/misc.py +22 -0
  20. aiogram_testing-1.2.0/aiogram_tests/types/dataset/payments.py +64 -0
  21. {aiogram_testing-1.1.0 → aiogram_testing-1.2.0}/pyproject.toml +7 -3
  22. aiogram_testing-1.1.0/PKG-INFO +0 -86
  23. aiogram_testing-1.1.0/README.md +0 -64
  24. aiogram_testing-1.1.0/aiogram_tests/__init__.py +0 -4
  25. aiogram_testing-1.1.0/aiogram_tests/exceptions.py +0 -2
  26. aiogram_testing-1.1.0/aiogram_tests/handler/__init__.py +0 -6
  27. aiogram_testing-1.1.0/aiogram_tests/handler/handler.py +0 -86
  28. aiogram_testing-1.1.0/aiogram_tests/requester.py +0 -94
  29. aiogram_testing-1.1.0/aiogram_tests/types/dataset/__init__.py +0 -673
  30. aiogram_testing-1.1.0/aiogram_tests/types/dataset/base.py +0 -68
  31. {aiogram_testing-1.1.0 → aiogram_testing-1.2.0}/aiogram_tests/types/__init__.py +0 -0
  32. {aiogram_testing-1.1.0 → aiogram_testing-1.2.0}/aiogram_tests/utils.py +0 -0
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: aiogram-testing
3
+ Version: 1.2.0
4
+ Summary: A library for testing your bots on aiogram
5
+ License: MIT
6
+ Author: k0te1ch
7
+ Author-email: khvostov40@gmail.com
8
+ Requires-Python: >=3.12,<3.15
9
+ Classifier: Framework :: AsyncIO
10
+ Classifier: Framework :: Pytest
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Software Development :: Testing
18
+ Requires-Dist: aiogram (>=3.23,<4)
19
+ Project-URL: Homepage, https://github.com/k0te1ch/aiogram_tests
20
+ Project-URL: Repository, https://github.com/k0te1ch/aiogram_tests
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Aiogram Tests
24
+
25
+ ***aiogram_tests*** is a testing library for bots written on [aiogram](https://github.com/aiogram/aiogram)
26
+
27
+ ## 📦 Installation
28
+
29
+ ```bash
30
+ pip install aiogram-testing
31
+ ```
32
+
33
+ The import name stays `aiogram_tests`:
34
+
35
+ ```python
36
+ from aiogram_tests import MockedRequester
37
+ ```
38
+
39
+ ## 📚 Simple examples
40
+
41
+ ### Simple handler test
42
+
43
+ #### Simple bot
44
+
45
+ ```python
46
+ from aiogram import Bot, Dispatcher, types
47
+ from aiogram.fsm.context import FSMContext
48
+
49
+ # Please, keep your bot tokens on environments, this code only example
50
+ bot = Bot('123456789:AABBCCDDEEFFaabbccddeeff-1234567890')
51
+ dp = Dispatcher()
52
+
53
+
54
+ @dp.message()
55
+ async def echo(message: types.Message, state: FSMContext) -> None:
56
+ await message.answer(message.text)
57
+
58
+
59
+ if __name__ == '__main__':
60
+ dp.run_polling(bot)
61
+
62
+
63
+ ```
64
+
65
+ #### Test cases
66
+
67
+ ```python
68
+ import pytest
69
+
70
+ from bot import echo
71
+
72
+ from aiogram_tests import MockedRequester
73
+ from aiogram_tests.handler import MessageHandler
74
+ from aiogram_tests.types.dataset import MESSAGE
75
+
76
+
77
+ @pytest.mark.asyncio
78
+ async def test_echo():
79
+ request = MockedRequester(MessageHandler(echo))
80
+ calls = await request.query(message=MESSAGE.as_object(text="Hello, Bot!"))
81
+ answer_message = calls.send_message.fetchone()
82
+ assert answer_message.text == "Hello, Bot!"
83
+
84
+ ```
85
+
86
+ ### Checking calls
87
+
88
+ `query()` returns the Bot API methods the handler called, as aiogram objects (`SendMessage`, `EditMessageText`, ...),
89
+ so fields such as `reply_markup` are typed models, not raw dicts.
90
+
91
+ ```python
92
+ from aiogram.methods import AnswerCallbackQuery, SendMessage
93
+
94
+ calls = await request.query(message=MESSAGE.as_object(text="Hello, Bot!"))
95
+
96
+ calls.send_message.fetchone() # last SendMessage, by snake_case name
97
+ calls.get(SendMessage) # all SendMessage calls, typed
98
+ calls.last(SendMessage) # last one or None
99
+ calls.assert_called(SendMessage, text="Hello, Bot!") # fails with the calls that were made
100
+ calls.assert_not_called(AnswerCallbackQuery)
101
+ ```
102
+
103
+ Accessing a method that was not called (`calls.edit_message_text`) raises `MethodIsNotCalledError`, an
104
+ `AttributeError`, so `hasattr(calls, "edit_message_text")` works.
105
+
106
+ ### Testing a whole router: `BotTester`
107
+
108
+ `MockedRequester` checks one handler function. `BotTester` runs updates through your real router or dispatcher, with
109
+ its filters, middlewares, FSM and injected dependencies, just like production:
110
+
111
+ ```python
112
+ from aiogram.methods import SendMessage
113
+
114
+ from mybot.handlers import router
115
+
116
+
117
+ async def test_survey(bot_tester): # fixture from the bundled pytest plugin
118
+ tester = bot_tester(router, db=fake_db) # keyword arguments reach handlers like dp.start_polling(**kwargs)
119
+
120
+ calls = await tester.send_message("/survey")
121
+ calls.assert_called(SendMessage, text="What is your name?")
122
+
123
+ await tester.send_message("Alice") # FSM state carries over between steps
124
+ calls = await tester.press("Python") # presses a button from the last inline keyboard
125
+
126
+ calls.assert_called(SendMessage, text="Nice to meet you, Alice!")
127
+ assert await tester.get_state() is None
128
+ ```
129
+
130
+ - `send_message(text, user=..., chat=...)`, `click(data_or_callback_data, message=..., user=...)`, `press(button_text)`
131
+ and `feed(event_or_update, event_type=...)` return the calls made while handling that update.
132
+ - `get_state(user)`, `set_state(state, user=..., **data)` and `state(user)` give access to the FSM.
133
+ - A module-level router is detached from the previous test's dispatcher, so each test gets a fresh one.
134
+ - A `Dispatcher` is used as is; its `workflow_data` and middlewares stay in place.
135
+
136
+ The plugin is registered automatically on install and provides the `bot_tester` (factory), `mocked_bot` and
137
+ `dataset` fixtures. See [examples/test_survey.py](examples/test_survey.py).
138
+
139
+ ### Other update types
140
+
141
+ Each update type has its handler; pass the event positionally or by its update field name:
142
+
143
+ ```python
144
+ from aiogram_tests.handler import InlineQueryHandler, PreCheckoutQueryHandler, UpdateHandler
145
+ from aiogram_tests.types.dataset import INLINE_QUERY, PRE_CHECKOUT_QUERY
146
+
147
+ calls = await MockedRequester(InlineQueryHandler(search)).query(INLINE_QUERY.as_object(query="cats"))
148
+ calls = await MockedRequester(PreCheckoutQueryHandler(approve)).query(pre_checkout_query=PRE_CHECKOUT_QUERY.as_object())
149
+
150
+ # anything else the dispatcher knows, by name
151
+ calls = await MockedRequester(UpdateHandler(on_business, event="business_message")).query(message)
152
+ ```
153
+
154
+ Available: `MessageHandler`, `EditedMessageHandler`, `ChannelPostHandler`, `EditedChannelPostHandler`,
155
+ `CallbackQueryHandler`, `InlineQueryHandler`, `ChosenInlineResultHandler`, `ShippingQueryHandler`,
156
+ `PreCheckoutQueryHandler`, `PollHandler`, `PollAnswerHandler`, `MyChatMemberHandler`, `ChatMemberHandler`,
157
+ `ChatJoinRequestHandler`, `MessageReactionHandler` and `UpdateHandler`.
158
+
159
+ Middlewares passed as `dp_middlewares` are registered on every event observer except `update` and `error`, whose
160
+ events are `Update` and `ErrorEvent` rather than messages or callbacks.
161
+
162
+ ### [▶️ More](https://github.com/k0te1ch/aiogram_tests/tree/main/examples) examples
163
+
@@ -0,0 +1,140 @@
1
+ # Aiogram Tests
2
+
3
+ ***aiogram_tests*** is a testing library for bots written on [aiogram](https://github.com/aiogram/aiogram)
4
+
5
+ ## 📦 Installation
6
+
7
+ ```bash
8
+ pip install aiogram-testing
9
+ ```
10
+
11
+ The import name stays `aiogram_tests`:
12
+
13
+ ```python
14
+ from aiogram_tests import MockedRequester
15
+ ```
16
+
17
+ ## 📚 Simple examples
18
+
19
+ ### Simple handler test
20
+
21
+ #### Simple bot
22
+
23
+ ```python
24
+ from aiogram import Bot, Dispatcher, types
25
+ from aiogram.fsm.context import FSMContext
26
+
27
+ # Please, keep your bot tokens on environments, this code only example
28
+ bot = Bot('123456789:AABBCCDDEEFFaabbccddeeff-1234567890')
29
+ dp = Dispatcher()
30
+
31
+
32
+ @dp.message()
33
+ async def echo(message: types.Message, state: FSMContext) -> None:
34
+ await message.answer(message.text)
35
+
36
+
37
+ if __name__ == '__main__':
38
+ dp.run_polling(bot)
39
+
40
+
41
+ ```
42
+
43
+ #### Test cases
44
+
45
+ ```python
46
+ import pytest
47
+
48
+ from bot import echo
49
+
50
+ from aiogram_tests import MockedRequester
51
+ from aiogram_tests.handler import MessageHandler
52
+ from aiogram_tests.types.dataset import MESSAGE
53
+
54
+
55
+ @pytest.mark.asyncio
56
+ async def test_echo():
57
+ request = MockedRequester(MessageHandler(echo))
58
+ calls = await request.query(message=MESSAGE.as_object(text="Hello, Bot!"))
59
+ answer_message = calls.send_message.fetchone()
60
+ assert answer_message.text == "Hello, Bot!"
61
+
62
+ ```
63
+
64
+ ### Checking calls
65
+
66
+ `query()` returns the Bot API methods the handler called, as aiogram objects (`SendMessage`, `EditMessageText`, ...),
67
+ so fields such as `reply_markup` are typed models, not raw dicts.
68
+
69
+ ```python
70
+ from aiogram.methods import AnswerCallbackQuery, SendMessage
71
+
72
+ calls = await request.query(message=MESSAGE.as_object(text="Hello, Bot!"))
73
+
74
+ calls.send_message.fetchone() # last SendMessage, by snake_case name
75
+ calls.get(SendMessage) # all SendMessage calls, typed
76
+ calls.last(SendMessage) # last one or None
77
+ calls.assert_called(SendMessage, text="Hello, Bot!") # fails with the calls that were made
78
+ calls.assert_not_called(AnswerCallbackQuery)
79
+ ```
80
+
81
+ Accessing a method that was not called (`calls.edit_message_text`) raises `MethodIsNotCalledError`, an
82
+ `AttributeError`, so `hasattr(calls, "edit_message_text")` works.
83
+
84
+ ### Testing a whole router: `BotTester`
85
+
86
+ `MockedRequester` checks one handler function. `BotTester` runs updates through your real router or dispatcher, with
87
+ its filters, middlewares, FSM and injected dependencies, just like production:
88
+
89
+ ```python
90
+ from aiogram.methods import SendMessage
91
+
92
+ from mybot.handlers import router
93
+
94
+
95
+ async def test_survey(bot_tester): # fixture from the bundled pytest plugin
96
+ tester = bot_tester(router, db=fake_db) # keyword arguments reach handlers like dp.start_polling(**kwargs)
97
+
98
+ calls = await tester.send_message("/survey")
99
+ calls.assert_called(SendMessage, text="What is your name?")
100
+
101
+ await tester.send_message("Alice") # FSM state carries over between steps
102
+ calls = await tester.press("Python") # presses a button from the last inline keyboard
103
+
104
+ calls.assert_called(SendMessage, text="Nice to meet you, Alice!")
105
+ assert await tester.get_state() is None
106
+ ```
107
+
108
+ - `send_message(text, user=..., chat=...)`, `click(data_or_callback_data, message=..., user=...)`, `press(button_text)`
109
+ and `feed(event_or_update, event_type=...)` return the calls made while handling that update.
110
+ - `get_state(user)`, `set_state(state, user=..., **data)` and `state(user)` give access to the FSM.
111
+ - A module-level router is detached from the previous test's dispatcher, so each test gets a fresh one.
112
+ - A `Dispatcher` is used as is; its `workflow_data` and middlewares stay in place.
113
+
114
+ The plugin is registered automatically on install and provides the `bot_tester` (factory), `mocked_bot` and
115
+ `dataset` fixtures. See [examples/test_survey.py](examples/test_survey.py).
116
+
117
+ ### Other update types
118
+
119
+ Each update type has its handler; pass the event positionally or by its update field name:
120
+
121
+ ```python
122
+ from aiogram_tests.handler import InlineQueryHandler, PreCheckoutQueryHandler, UpdateHandler
123
+ from aiogram_tests.types.dataset import INLINE_QUERY, PRE_CHECKOUT_QUERY
124
+
125
+ calls = await MockedRequester(InlineQueryHandler(search)).query(INLINE_QUERY.as_object(query="cats"))
126
+ calls = await MockedRequester(PreCheckoutQueryHandler(approve)).query(pre_checkout_query=PRE_CHECKOUT_QUERY.as_object())
127
+
128
+ # anything else the dispatcher knows, by name
129
+ calls = await MockedRequester(UpdateHandler(on_business, event="business_message")).query(message)
130
+ ```
131
+
132
+ Available: `MessageHandler`, `EditedMessageHandler`, `ChannelPostHandler`, `EditedChannelPostHandler`,
133
+ `CallbackQueryHandler`, `InlineQueryHandler`, `ChosenInlineResultHandler`, `ShippingQueryHandler`,
134
+ `PreCheckoutQueryHandler`, `PollHandler`, `PollAnswerHandler`, `MyChatMemberHandler`, `ChatMemberHandler`,
135
+ `ChatJoinRequestHandler`, `MessageReactionHandler` and `UpdateHandler`.
136
+
137
+ Middlewares passed as `dp_middlewares` are registered on every event observer except `update` and `error`, whose
138
+ events are `Update` and `ErrorEvent` rather than messages or callbacks.
139
+
140
+ ### [▶️ More](https://github.com/k0te1ch/aiogram_tests/tree/main/examples) examples
@@ -0,0 +1,6 @@
1
+ from .requester import Calls
2
+ from .requester import MockedRequester
3
+ from .tester import BotTester
4
+
5
+ __all__ = ["BotTester", "Calls", "MockedRequester"]
6
+ __version__ = "1.2.0" # x-release-please-version
@@ -0,0 +1,6 @@
1
+ class MethodIsNotCalledError(AttributeError):
2
+ pass
3
+
4
+
5
+ class MockedResponseMissingError(Exception):
6
+ pass
@@ -0,0 +1,39 @@
1
+ from .base import RequestHandler
2
+ from .handler import CallbackQueryHandler
3
+ from .handler import ChannelPostHandler
4
+ from .handler import ChatJoinRequestHandler
5
+ from .handler import ChatMemberHandler
6
+ from .handler import ChosenInlineResultHandler
7
+ from .handler import EditedChannelPostHandler
8
+ from .handler import EditedMessageHandler
9
+ from .handler import InlineQueryHandler
10
+ from .handler import MessageHandler
11
+ from .handler import MessageReactionHandler
12
+ from .handler import MyChatMemberHandler
13
+ from .handler import PollAnswerHandler
14
+ from .handler import PollHandler
15
+ from .handler import PreCheckoutQueryHandler
16
+ from .handler import ShippingQueryHandler
17
+ from .handler import TelegramEventObserverHandler
18
+ from .handler import UpdateHandler
19
+
20
+ __all__ = [
21
+ "CallbackQueryHandler",
22
+ "ChannelPostHandler",
23
+ "ChatJoinRequestHandler",
24
+ "ChatMemberHandler",
25
+ "ChosenInlineResultHandler",
26
+ "EditedChannelPostHandler",
27
+ "EditedMessageHandler",
28
+ "InlineQueryHandler",
29
+ "MessageHandler",
30
+ "MessageReactionHandler",
31
+ "MyChatMemberHandler",
32
+ "PollAnswerHandler",
33
+ "PollHandler",
34
+ "PreCheckoutQueryHandler",
35
+ "RequestHandler",
36
+ "ShippingQueryHandler",
37
+ "TelegramEventObserverHandler",
38
+ "UpdateHandler",
39
+ ]
@@ -2,7 +2,6 @@ from collections.abc import Iterable
2
2
 
3
3
  from aiogram import BaseMiddleware
4
4
  from aiogram import Dispatcher
5
- from aiogram.dispatcher.event.telegram import TelegramEventObserver
6
5
  from aiogram.fsm.storage.memory import MemoryStorage
7
6
  from aiogram.methods import TelegramMethod
8
7
  from aiogram.methods.base import Response
@@ -39,14 +38,12 @@ class RequestHandler:
39
38
  def _get_dispatcher_event_observers(self) -> list[str]:
40
39
  """
41
40
  Returns a names for bot event observers, like message, callback_query etc.
42
- """
43
41
 
44
- result = []
45
- for name in dir(self.dp):
46
- if isinstance(getattr(self.dp, name), TelegramEventObserver):
47
- result.append(name)
42
+ ``update`` and ``error`` are left out: their events are ``Update`` and ``ErrorEvent``, and a middleware
43
+ written for messages or callbacks breaks on them. Register such middlewares on ``dp`` directly.
44
+ """
48
45
 
49
- return result
46
+ return [name for name in self.dp.observers if name not in ("update", "error")]
50
47
 
51
48
  def _register_middlewares(self, event_observer: Iterable, middlewares: Iterable) -> None:
52
49
  for eo_name in event_observer:
@@ -0,0 +1,178 @@
1
+ import itertools
2
+ from collections.abc import Callable
3
+ from collections.abc import Iterable
4
+ from typing import Any
5
+
6
+ from aiogram import types
7
+ from aiogram.dispatcher.middlewares.user_context import UserContextMiddleware
8
+ from aiogram.filters import Filter
9
+ from aiogram.filters import StateFilter
10
+ from aiogram.fsm.context import FSMContext
11
+ from aiogram.fsm.state import State
12
+
13
+ from aiogram_tests.types.dataset import CHAT
14
+ from aiogram_tests.types.dataset import USER
15
+
16
+ from .base import RequestHandler
17
+
18
+
19
+ class TelegramEventObserverHandler(RequestHandler):
20
+ """
21
+ Registers ``callback`` on the dispatcher observer ``event`` (``message``, ``inline_query``, ...) and feeds it
22
+ updates of that type. Subclasses fix ``event``; ``UpdateHandler`` takes it as an argument.
23
+ """
24
+
25
+ event: str | None = None
26
+
27
+ def __init__(
28
+ self,
29
+ callback: Callable,
30
+ *filters: Filter,
31
+ state: State | str | None = None,
32
+ state_data: dict = None,
33
+ state_context: FSMContext | None = None,
34
+ dp_middlewares: Iterable = None,
35
+ exclude_observer_methods: Iterable = None,
36
+ **kwargs,
37
+ ):
38
+ super().__init__(dp_middlewares, exclude_observer_methods, **kwargs)
39
+
40
+ if state_data is None:
41
+ state_data = {}
42
+ if not isinstance(state_data, dict):
43
+ raise ValueError("state_data is not a dict")
44
+
45
+ self._callback = callback
46
+ self._filters: list = list(filters)
47
+ self._state: State | str | None = state
48
+ self._state_data: dict = state_data
49
+ self._state_context: FSMContext | None = state_context
50
+ self._registered = False
51
+ self._update_ids = itertools.count(1)
52
+
53
+ if self._state:
54
+ self._filters.append(StateFilter(self._state))
55
+
56
+ async def __call__(self, *args, **kwargs):
57
+ if not self._registered:
58
+ self.register_handler()
59
+ self._registered = True
60
+
61
+ update = self.build_update(*args, **kwargs)
62
+
63
+ if self._state_context or self._state:
64
+ state = self._get_state_context(update)
65
+ await state.set_state(self._state)
66
+ await state.update_data(**self._state_data)
67
+
68
+ await self.dp.feed_update(self.bot, update)
69
+
70
+ def _get_state_context(self, update: types.Update) -> FSMContext:
71
+ """
72
+ FSM context of the user and chat the update comes from, as the dispatcher resolves it
73
+ """
74
+
75
+ context = UserContextMiddleware.resolve_event_context(update)
76
+ state = self.dp.fsm.resolve_context(
77
+ bot=self.bot,
78
+ chat_id=context.chat_id or CHAT["id"],
79
+ user_id=context.user_id or USER["id"],
80
+ thread_id=context.thread_id,
81
+ business_connection_id=context.business_connection_id,
82
+ )
83
+ return state
84
+
85
+ def register_handler(self) -> None:
86
+ """
87
+ Register TelegramEventObserver in dispatcher
88
+ """
89
+
90
+ getattr(self.dp, self.event).register(self._callback, *self._filters)
91
+
92
+ def build_update(self, *args, **kwargs) -> types.Update:
93
+ """
94
+ Wrap the event, passed positionally or by its update field name, into an update for the dispatcher
95
+ """
96
+
97
+ event = self._pick_event(args, kwargs)
98
+ return types.Update(update_id=next(self._update_ids), **{self.event: event})
99
+
100
+ def _pick_event(self, args: tuple, kwargs: dict) -> Any:
101
+ if args and not kwargs:
102
+ return args[0]
103
+ if not args and list(kwargs) == [self.event]:
104
+ return kwargs[self.event]
105
+ passed = [*(type(arg).__name__ for arg in args), *kwargs]
106
+ raise TypeError(f"{type(self).__name__} takes one '{self.event}' event, got {passed}")
107
+
108
+
109
+ class UpdateHandler(TelegramEventObserverHandler):
110
+ """
111
+ Handler for any update type by its field name: ``UpdateHandler(callback, event="business_message")``
112
+ """
113
+
114
+ def __init__(self, callback: Callable, *filters: Filter, event: str, **kwargs):
115
+ self.event = event
116
+ super().__init__(callback, *filters, **kwargs)
117
+ if event not in self.dp.observers or event in ("update", "error"):
118
+ raise ValueError(f"dispatcher has no '{event}' updates")
119
+
120
+
121
+ class MessageHandler(TelegramEventObserverHandler):
122
+ event = "message"
123
+
124
+
125
+ class EditedMessageHandler(TelegramEventObserverHandler):
126
+ event = "edited_message"
127
+
128
+
129
+ class ChannelPostHandler(TelegramEventObserverHandler):
130
+ event = "channel_post"
131
+
132
+
133
+ class EditedChannelPostHandler(TelegramEventObserverHandler):
134
+ event = "edited_channel_post"
135
+
136
+
137
+ class CallbackQueryHandler(TelegramEventObserverHandler):
138
+ event = "callback_query"
139
+
140
+
141
+ class InlineQueryHandler(TelegramEventObserverHandler):
142
+ event = "inline_query"
143
+
144
+
145
+ class ChosenInlineResultHandler(TelegramEventObserverHandler):
146
+ event = "chosen_inline_result"
147
+
148
+
149
+ class ShippingQueryHandler(TelegramEventObserverHandler):
150
+ event = "shipping_query"
151
+
152
+
153
+ class PreCheckoutQueryHandler(TelegramEventObserverHandler):
154
+ event = "pre_checkout_query"
155
+
156
+
157
+ class PollHandler(TelegramEventObserverHandler):
158
+ event = "poll"
159
+
160
+
161
+ class PollAnswerHandler(TelegramEventObserverHandler):
162
+ event = "poll_answer"
163
+
164
+
165
+ class MyChatMemberHandler(TelegramEventObserverHandler):
166
+ event = "my_chat_member"
167
+
168
+
169
+ class ChatMemberHandler(TelegramEventObserverHandler):
170
+ event = "chat_member"
171
+
172
+
173
+ class ChatJoinRequestHandler(TelegramEventObserverHandler):
174
+ event = "chat_join_request"
175
+
176
+
177
+ class MessageReactionHandler(TelegramEventObserverHandler):
178
+ event = "message_reaction"
@@ -11,6 +11,8 @@ from aiogram.types import ResponseParameters
11
11
  from aiogram.types import User
12
12
  from aiogram.types.base import UNSET_TYPE
13
13
 
14
+ from .exceptions import MockedResponseMissingError
15
+
14
16
  DEFAULT_AUTO_MOCK_SUCCESS = True
15
17
 
16
18
 
@@ -19,6 +21,7 @@ class MockedSession(BaseSession):
19
21
  super().__init__()
20
22
  self.responses: deque[Response[TelegramType]] = deque()
21
23
  self.requests: deque[Request] = deque()
24
+ self.methods: deque[TelegramMethod] = deque()
22
25
  self.closed = True
23
26
 
24
27
  def add_result(self, response: Response[TelegramType]) -> Response[TelegramType]:
@@ -43,6 +46,12 @@ class MockedSession(BaseSession):
43
46
  self.closed = False
44
47
  request = Request(method=method.__api_method__, data=method.__dict__, files=None)
45
48
  self.requests.append(request)
49
+ self.methods.append(method)
50
+ if not self.responses:
51
+ raise MockedResponseMissingError(
52
+ f"no mocked response for {method.__api_method__}: call "
53
+ f"add_result_for({type(method).__name__}, ...) first or enable auto_mock_success"
54
+ )
46
55
  response: Response[TelegramType] = self.responses.pop()
47
56
  self.check_response(
48
57
  method=method,
@@ -61,7 +70,6 @@ class MockedSession(BaseSession):
61
70
  class MockedBot(Bot):
62
71
  def __init__(self, auto_mock_success: bool = DEFAULT_AUTO_MOCK_SUCCESS, **kwargs):
63
72
  super().__init__(kwargs.pop("token", "42:TEST"), session=MockedSession(), **kwargs)
64
- self.session = MockedSession()
65
73
  self._me = User(
66
74
  id=self.id,
67
75
  is_bot=True,
@@ -0,0 +1,36 @@
1
+ """pytest fixtures, loaded automatically through the ``pytest11`` entry point"""
2
+
3
+ from collections.abc import Callable
4
+ from types import ModuleType
5
+ from typing import Any
6
+
7
+ import pytest
8
+ from aiogram import Dispatcher
9
+ from aiogram import Router
10
+
11
+ from .mocked_bot import MockedBot
12
+ from .tester import BotTester
13
+ from .types import dataset as _dataset
14
+
15
+
16
+ @pytest.fixture
17
+ def mocked_bot() -> MockedBot:
18
+ return MockedBot()
19
+
20
+
21
+ @pytest.fixture
22
+ def bot_tester(mocked_bot: MockedBot) -> Callable[..., BotTester]:
23
+ """
24
+ Factory: ``bot_tester(router_or_dispatcher, **workflow_data)``; every tester in a test shares ``mocked_bot``
25
+ """
26
+
27
+ def make(dispatcher: Dispatcher | Router, **kwargs: Any) -> BotTester:
28
+ kwargs.setdefault("bot", mocked_bot)
29
+ return BotTester(dispatcher, **kwargs)
30
+
31
+ return make
32
+
33
+
34
+ @pytest.fixture
35
+ def dataset() -> ModuleType:
36
+ return _dataset