Skip to content

Commit ae6b214

Browse files
authored
Added utilities method for inmemory broker. (#388)
* Added utilities method for inmemory broker. * Added docs for new utilities.
1 parent e5c6d2b commit ae6b214

File tree

3 files changed

+118
-7
lines changed

3 files changed

+118
-7
lines changed

docs/guide/testing-taskiq.md

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ the same interface as a real broker, but it doesn't send tasks actually.
8383
Let's define a task.
8484

8585
```python
86-
from your_project.taskiq import broker
86+
from your_project.tkq import broker
8787

8888
@broker.task
8989
async def parse_int(val: str) -> int:
@@ -107,7 +107,7 @@ And that's it. Test should pass.
107107
What if you want to test a function that uses task. Let's define such function.
108108

109109
```python
110-
from your_project.taskiq import broker
110+
from your_project.tkq import broker
111111

112112
@broker.task
113113
async def parse_int(val: str) -> int:
@@ -129,6 +129,63 @@ async def test_add_one():
129129
assert await parse_and_add_one("11") == 12
130130
```
131131

132+
### Unawaitable tasks
133+
134+
When a function calls an asynchronous task but doesn't await its result,
135+
it can be challenging to test.
136+
137+
In such cases, the `InMemoryBroker` provides two convenient ways to help you:
138+
the `await_inplace` constructor parameter and the `wait_all` method.
139+
140+
Consider the following example where we define a task and a function that calls it:
141+
142+
```python
143+
from your_project.tkq import broker
144+
145+
@broker.task
146+
async def parse_int(val: str) -> int:
147+
return int(val)
148+
149+
150+
async def parse_int_later(val: str) -> int:
151+
await parse_int.kiq(val)
152+
return 1
153+
```
154+
155+
To test this function, we can do two things:
156+
157+
1. By setting the `await_inplace=True` parameter when creating the broker.
158+
In that case all tasks will be automatically awaited as soon as they are called.
159+
In such a way you don't need to manually call the `wait_result` in your code.
160+
161+
To set it up, define the broker as the following:
162+
163+
```python
164+
...
165+
broker = InMemoryBroker(await_inplace=True)
166+
...
167+
168+
```
169+
170+
With this setup all `await function.kiq()` calls will behave similarly to `await function()`, but
171+
with dependency injection and all taskiq-related functionality.
172+
173+
2. Alternatively, you can manually await all tasks after invoking the
174+
target function by using the `wait_all` method.
175+
This gives you more control over when to wait for tasks to complete.
176+
177+
```python
178+
from your_project.tkq import broker
179+
180+
@pytest.mark.anyio
181+
async def test_add_one():
182+
# Call the function that triggers the async task
183+
assert await parse_int_later("11") == 1
184+
await broker.wait_all() # Waits for all tasks to complete
185+
# At that time we can guarantee that all sent tasks
186+
# have been completed and do all the assertions.
187+
```
188+
132189
## Dependency injection
133190

134191
If you use dependencies in your tasks, you may think that this can become a problem. But it's not.
@@ -146,7 +203,7 @@ from typing import Annotated
146203
from pathlib import Path
147204
from taskiq import TaskiqDepends
148205

149-
from your_project.taskiq import broker
206+
from your_project.tkq import broker
150207

151208

152209
@broker.task
@@ -161,7 +218,7 @@ async def modify_path(some_path: Annotated[Path, TaskiqDepends()]):
161218
from pathlib import Path
162219
from taskiq import TaskiqDepends
163220

164-
from your_project.taskiq import broker
221+
from your_project.tkq import broker
165222

166223

167224
@broker.task
@@ -177,7 +234,7 @@ expected dependencies manually as function's arguments or key-word arguments.
177234

178235
```python
179236
import pytest
180-
from your_project.taskiq import broker
237+
from your_project.tkq import broker
181238

182239
from pathlib import Path
183240

@@ -193,7 +250,7 @@ must mutate dependency_context before calling a task. We suggest to do it in fix
193250

194251
```python
195252
import pytest
196-
from your_project.taskiq import broker
253+
from your_project.tkq import broker
197254
from pathlib import Path
198255

199256

taskiq/brokers/inmemory_broker.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ def __init__(
127127
cast_types: bool = True,
128128
max_async_tasks: int = 30,
129129
propagate_exceptions: bool = True,
130+
await_inplace: bool = False,
130131
) -> None:
131132
super().__init__()
132133
self.result_backend = InmemoryResultBackend(
@@ -140,6 +141,7 @@ def __init__(
140141
max_async_tasks=max_async_tasks,
141142
propagate_exceptions=propagate_exceptions,
142143
)
144+
self.await_inplace = await_inplace
143145
self._running_tasks: "Set[asyncio.Task[Any]]" = set()
144146

145147
async def kick(self, message: BrokerMessage) -> None:
@@ -156,7 +158,12 @@ async def kick(self, message: BrokerMessage) -> None:
156158
if target_task is None:
157159
raise TaskiqError("Unknown task.")
158160

159-
task = asyncio.create_task(self.receiver.callback(message=message.message))
161+
receiver_cb = self.receiver.callback(message=message.message)
162+
if self.await_inplace:
163+
await receiver_cb
164+
return
165+
166+
task = asyncio.create_task(receiver_cb)
160167
self._running_tasks.add(task)
161168
task.add_done_callback(self._running_tasks.discard)
162169

@@ -171,6 +178,17 @@ def listen(self) -> AsyncGenerator[bytes, None]:
171178
"""
172179
raise RuntimeError("Inmemory brokers cannot listen.")
173180

181+
async def wait_all(self) -> None:
182+
"""
183+
Wait for all currently running tasks to complete.
184+
185+
Useful when used in testing and you need to await all sent tasks
186+
before asserting results.
187+
"""
188+
to_await = list(self._running_tasks)
189+
for task in to_await:
190+
await task
191+
174192
async def startup(self) -> None:
175193
"""Runs startup events for client and worker side."""
176194
for event in (TaskiqEvents.CLIENT_STARTUP, TaskiqEvents.WORKER_STARTUP):

tests/brokers/test_inmemory.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,39 @@ async def test_task() -> str:
8585

8686
result = await task.wait_result()
8787
assert result.return_value == test_value
88+
89+
90+
@pytest.mark.anyio
91+
async def test_inline_awaits() -> None:
92+
broker = InMemoryBroker(await_inplace=True)
93+
slept = False
94+
95+
@broker.task
96+
async def test_task() -> None:
97+
nonlocal slept
98+
await asyncio.sleep(0.2)
99+
slept = True
100+
101+
task = await test_task.kiq()
102+
assert slept
103+
assert await task.is_ready()
104+
assert not broker._running_tasks
105+
106+
107+
@pytest.mark.anyio
108+
async def test_wait_all() -> None:
109+
broker = InMemoryBroker()
110+
slept = False
111+
112+
@broker.task
113+
async def test_task() -> None:
114+
nonlocal slept
115+
await asyncio.sleep(0.2)
116+
slept = True
117+
118+
task = await test_task.kiq()
119+
assert not slept
120+
await broker.wait_all()
121+
assert slept
122+
assert await task.is_ready()
123+
assert not broker._running_tasks

0 commit comments

Comments
 (0)