下面的示例类有一个可等待的属性
bar
,如 async_main()
所示,因为它(理论上)在返回所有内容的答案之前会执行一些 IO 工作。
class Foo:
@property
async def bar(self):
return 42
async def async_main():
f = Foo()
print(await f.bar)
我在测试它时遇到了麻烦,因为通常怀疑的 Mock、MagicMock 和 AsyncMock 不能按预期使用属性。我当前的解决方法是:
f.bar = some_awaitable()
因为这使得 f.bar 成为一个可以等待的“字段”,但不幸的是我需要在测试时多次访问它,这当然会在第二次访问时产生
RuntimeError: cannot reuse already awaited coroutine
。
是否有一种既定的方法来模拟这样的异步属性?
我能想到的最简单的方法是使用
bar
属性再次修补 async
以便进行测试。
我假设您想要测试
Foo
上的其他方法,并且该方法调用其 bar
。
code.py
from asyncio import run
class Foo:
@property
async def bar(self) -> int:
return 42
async def func(self) -> int:
return await self.bar
async def main():
f = Foo()
print(await f.func())
if __name__ == '__main__':
run(main())
test.py
from unittest import IsolatedAsyncioTestCase
from unittest.mock import patch
from . import code
class FooTestCase(IsolatedAsyncioTestCase):
async def test_func(self) -> None:
expected_output = 69420
@property
async def mock_bar(_foo_self: code.Foo) -> int:
return expected_output
with patch.object(code.Foo, "bar", new=mock_bar):
f = code.Foo()
# Just to see that our mocking worked:
self.assertEqual(expected_output, await f.bar)
# Should call `bar` property again:
output = await f.func()
self.assertEqual(expected_output, output)
patch
文档。
属性
Foo.bar
没有 setter,因此类 Foo
的实例无法将模拟对象分配给该属性。适当的解决方案是用 Foo.bar
修补 PropertyMock
。
import asyncio
from unittest import mock
async def test_bar():
expected_value = 'expected value'
async_class_mock = mock.AsyncMock(**{'coroutine.return_value': expected_value})
task = asyncio.create_task(async_class_mock.coroutine())
patch_property_with = mock.PropertyMock(spec=Foo, **{'bar': task})
with mock.patch.object(target=Foo, attribute='bar', new=patch_property_with) as bar_mock:
Foo.bar = bar_mock.bar
assert await Foo().bar == expected_value
这是发生的事情:
async_class_mock
是 AsyncMock
的实例,已使用伪协程初始化,其返回值设置为测试期望 Foo.bar
返回的值。>>> async_class_mock.coroutine()
<coroutine object AsyncMockMixin._execute_mock_call at 0x00>
>>> await async_class_mock.coroutine()
'expected value'
asyncio.create_task
将安排在等待任务时执行 async_class_mock.coroutine()
。因为它是一个任务,所以你可以等待多次。>>> task
<Task pending name='Task-1' coro=<AsyncMockMixin._execute_mock_call() running at ...>>
>>> await task
'expected value'
>>> await task
'expected value'
patch_property_with
是 PropertyMock
的一个实例,它被分配给 Foo.bar
。它正在重新定义 Foo.bar
的含义。>>> bar_mock
<PropertyMock spec='Foo' ...>
>>> bar_mock.bar
<Task pending name='Task-1' coro=<AsyncMockMixin._execute_mock_call() running at ...>>
>>> Foo.bar = bar_mock.bar
现在
Foo.bar
引用异步任务,可以等待多次以获得预期值。
>>> foo = Foo()
>>> await foo.bar
'expected value'
>>> await foo.bar == expected_value
True
上下文管理器退出后,应用于
Foo.bar
的补丁将被删除。