Mocking async-вызов в python 3.5
Как мне высмеять асинхронный вызов из одной встроенной сопрограммы в другую с помощью unittest.mock.patch
?
В настоящее время у меня довольно неудобное решение:
class CoroutineMock(MagicMock):
def __await__(self, *args, **kwargs):
future = Future()
future.set_result(self)
result = yield from future
return result
Тогда
class TestCoroutines(TestCase):
@patch('some.path', new_callable=CoroutineMock)
def test(self, mock):
some_action()
mock.assert_called_with(1,2,3)
Это работает, но выглядит уродливо. Есть ли более питонический способ сделать это?
Ответы
Ответ 1
Подклассы MagicMock
распространят ваш пользовательский класс на все макеты, сгенерированные из вашего сопрограммного макета. Например, AsyncMock().__str__
также станет AsyncMock
что, вероятно, не то, что вы ищете.
Вместо этого вы можете захотеть определить фабрику, которая создает Mock
(или MagicMock
) с пользовательскими аргументами, например side_effect=coroutine(coro)
. Также может быть хорошей идеей отделить функцию сопрограммы от сопрограммы (как объяснено в документации).
Вот что я придумал:
from asyncio import coroutine
def CoroMock():
coro = Mock(name="CoroutineResult")
corofunc = Mock(name="CoroutineFunction", side_effect=coroutine(coro))
corofunc.coro = coro
return corofunc
Объяснение различных объектов:
-
corofunc
: макет функции сопрограммы -
corofunc.side_effect()
: сопрограмма, генерируемая для каждого вызова -
corofunc.coro
: макет, используемый сопрограммой для получения результата -
corofunc.coro.return_value
: значение, возвращаемое сопрограммой -
corofunc.coro.side_effect
: может использоваться для создания исключения
Пример:
async def coro(a, b):
return await sleep(1, result=a+b)
def some_action(a, b):
return get_event_loop().run_until_complete(coro(a, b))
@patch('__main__.coro', new_callable=CoroMock)
def test(corofunc):
a, b, c = 1, 2, 3
corofunc.coro.return_value = c
result = some_action(a, b)
corofunc.assert_called_with(a, b)
assert result == c
Ответ 2
Все упускают то, что, вероятно, самое простое и ясное решение:
@patch('some.path')
def test(self, mock):
f = asyncio.Future()
f.set_result('whatever result you want')
mock.return_value = f
mock.assert_called_with(1, 2, 3)
Помните, что сопрограмму можно рассматривать как функцию, гарантирующую возвращение будущего, которое, в свою очередь, можно ожидать.
Ответ 3
Решение было на самом деле довольно простым: мне просто нужно было преобразовать метод макета __call__
в сопрограмму:
class AsyncMock(MagicMock):
async def __call__(self, *args, **kwargs):
return super(AsyncMock, self).__call__(*args, **kwargs)
Это прекрасно работает, когда вызывается макет, код получает родную сопрограмму
Пример использования:
@mock.patch('my.path.asyncio.sleep', new_callable=AsyncMock)
def test_stuff(sleep):
# code
Ответ 4
Другой способ издевательства над сопрограммой состоит в том, чтобы сделать сопрограмму, которая возвращает насмешку. Таким образом вы можете высмеять сопрограммы, которые будут переданы в asyncio.wait
или asyncio.wait_for
.
Это делает более универсальные сопрограммы, хотя делает установку тестов более громоздкой:
def make_coroutine(mock)
async def coroutine(*args, **kwargs):
return mock(*args, **kwargs)
return coroutine
class Test(TestCase):
def setUp(self):
self.coroutine_mock = Mock()
self.patcher = patch('some.coroutine',
new=make_coroutine(self.coroutine_mock))
self.patcher.start()
def tearDown(self):
self.patcher.stop()
Ответ 5
Основываясь на ответе @scolvin, я создал этот (imo) более чистый способ:
def async_return(result):
f = asyncio.Future()
f.set_result(result)
return f
Что это, просто используйте это вокруг любого возврата, который вы хотите быть асинхронным, как в
mock = MagicMock(return_value=async_return("Example return"))
await mock()
Ответ 6
Еще один вариант "простейшего" решения для макетирования асинхронного объекта, который представляет собой всего один слой.
В источнике:
class Yo:
async def foo(self):
await self.bar()
async def bar(self):
# Some code
В тесте:
from asyncio import coroutine
yo = Yo()
# Here bounded method bar is mocked and will return a customised result.
yo.bar = Mock(side_effect=coroutine(lambda:'the awaitable should return this'))
event_loop.run_until_complete(yo.foo())