我正在写一些长时间运行的协程,它们相互作用。这些协程可以在await
上被阻塞,直到外部发生一些事情。我希望能够在单元测试中驱动这些协程。在协程上执行await
的常规方法不起作用。因为我希望能够在他们的操作中间拦截一些东西。我也不希望弄乱协程的内部构件,除非有一些通用的/可重用的东西可以做。
理想情况下,我希望 * 运行一个事件循环,直到所有的任务都被阻塞 *。这在事件循环实现中应该很容易判断。一旦所有的任务都被阻塞,事件循环就放弃控制,在那里我可以Assert一些关于协程的状态,并从外部戳它们。然后我可以继续循环,直到它再次被阻塞。这将允许在事件循环中对任务进行确定性模拟。
所需API的最小示例:
import asyncio
from asyncio import Event
# Imagine this is a complicated "main" with many coroutines.
# But event is some external "mockable" event
# that can be used to drive in unit tests
async def wait_on_event(event: Event):
print("Waiting on event")
await event.wait()
print("Done waiting on event")
def test_deterministic():
loop = asyncio.get_event_loop()
event = Event()
task = loop.create_task(wait_on_event(event))
run_until_blocked_or_complete(loop) # define this magic function
# Should print "Waiting on event"
# can make some test assertions here
event.set()
run_until_blocked_or_complete(loop)
# Should print "Done waiting on event"
任何类似的可能性?或者这需要编写一个定制的事件循环只是为了测试?
另外,我目前使用的是Python 3.9(AWS运行时限制),如果在Python 3.9中不能做到这一点,那么什么版本会支持呢?
5条答案
按热度按时间lqfhib0f1#
自从我第一次读到这个问题,这个问题就一直困扰着我,因为它几乎可以用标准的asyncio函数来实现,关键是 Alexandria 的“神奇”
is_not_blocked
方法,我在下面逐字地给予它(除了把它移到外部缩进级别)。我还使用了他的wait_on_event
方法,以及他的test_deterministic_loop
函数。我添加了一些额外的测试来展示如何启动和停止其他任务,以及如何逐步驱动事件循环直到完成所有任务。我没有使用DeterministicLoop类,而是使用了一个函数
run_until_blocked
,它只进行标准的异步函数调用。是一种方便的方法,可以将循环推进一个周期,并且asyncio已经提供了一种方法,可以获取在给定事件循环中运行的所有任务,因此不需要单独存储它们。
评论 Alexandria 的“魔法”方法:如果你看一下asyncio.task代码中的注解,“private”变量
_fut_waiter
被描述为一个重要的不变式。这在未来的版本中几乎不可能改变。所以我认为它在实践中是相当安全的。1yjd4xko2#
是的,你可以通过创建一个定制的事件循环策略和在测试中使用一个模拟事件循环来实现这一点,基本的想法是创建一个循环,它只运行到所有的协程都被阻塞为止,然后将控制权交还给测试代码来执行任何必要的Assert或外部戳,然后继续运行循环,直到所有的东西都被再次阻塞为止,等等。
此策略创建一个新的事件循环,该循环具有_blocked集属性,用于跟踪当前被阻止的任务。在循环中计划新任务时,将调用_enter_task方法,并将其添加到_blocked集中。完成或取消任务时,将调用_leave_task方法,并将其从_blocked集中删除。
run_until_blocked方法接受一个协程并运行事件循环,直到所有任务都被阻塞。它使用自定义策略创建一个新的事件循环,在循环上调度协程,然后重复运行循环,直到_blocked集为空。这是您可以执行任何必要Assert或外部插入的点。
以下是此策略的使用示例:
在这个测试中,我们创建了一个新的Event对象,并将其传递给wait_on_event协程。我们使用run_until_blocked方法运行协程,直到它阻塞event.wait()调用。此时,我们可以执行任何必要的Assert,例如检查事件是否尚未设置。然后,我们设置事件,并再次调用run_until_blocked以恢复协程,直到它完成。
此模式允许在事件循环中对任务进行确定性模拟,并可用于测试阻塞外部事件的协程。
希望这有帮助!
euoag5mw3#
默认的事件循环只是运行每个“过程”中安排的所有内容。如果您只是loop.call在任务运行后使用“www.example.com _soon”来安排暂停,则应该在所需的时间点调用您:
在REPL上运行这个:
55ooxyrt4#
经过一些实验,我想出了一些方法。下面是用法:
实施:
6jjcrrmo5#