#python #pytest
Вопрос:
Как я могу имитировать асинхронный вызов от одной встроенной сопрограммы к другой с помощью 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. Также этот макет не работает с asyncio.await из-за asyncio.tasks.ensure_future
Ответ №1:
Всем не хватает, вероятно, самого простого и ясного решения:
@patch('some.path')
def test(self, mock):
f = asyncio.Future()
f.set_result('whatever result you want')
process_smtp_message.return_value = f
mock.assert_called_with(1, 2, 3)
помните, что сопрограмму можно рассматривать как просто функцию, которая гарантированно возвращает будущее, которого, в свою очередь, можно ожидать.
Комментарии:
1. что такое process_smtp_message.return_value = f? Кроме того, где находится тестируемый вызов функции?
2. @Skorpeo — я думаю, он имеет в виду mock.return_value = f
3. Я определенно не знаю. Макет-это испытательное приспособление. process_smtp_message-это то, над чем вы, очевидно, пытаетесь издеваться.
4. Решение несовместимо с Python 3.8
AsyncMock
, в котором был представлен родной язык. Таким образом, код с решением завершится с ошибками из-за будущих проблем с классами. Но решение Zozz с простойAsyncMock
реализацией может работать в Python 3.7 (например) и Python 3.8 (если вы будете выполнять условный импорт родногоAsyncMock
языка )5. Для
python3.8
и выше я закончил тем, что использовал:patch.object(your_object, 'async_method_name', return_value=your_return_value)
Ответ №2:
Решение на самом деле было довольно простым: мне просто нужно было преобразовать __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
Комментарии:
1. Это хорошо, но не очень хорошо работает с автоспециализацией, которая в основном обязательна при использовании MagicMock. Есть какие-нибудь мысли о том, как заставить это работать? Я недостаточно знаком с внутренними органами…
2. Это прекрасно работает для меня. Я использовал его следующим образом: ` ‘ @mock.patch( ‘мой.путь.asyncio.sleep’, new_callable=AsyncMock, ) деф test_stuff(сон): # код «
3. Это работает. Сначала мне нравится другое решение, приведенное ниже Иваном Кастелланосом. Но будущее никогда не исполняется, и я изо всех сил старался, но у меня ничего не получилось.
4. Это настолько удобно и элегантно, насколько это возможно. По крайней мере, для базового использования.
5. Примечание:
AsyncMock
доступно вunittest.mock
версии python 3.8, mock также автоматически определяет, где его следует использовать ( примеры см. в документах ).
Ответ №3:
Основываясь на ответе @scolvin, я создал этот (imo) более чистый способ:
import asyncio
def async_return(result):
f = asyncio.Future()
f.set_result(result)
return f
Вот и все, просто используйте его вокруг любого возврата, который вы хотите сделать асинхронным, как в
mock = MagicMock(return_value=async_return("Example return"))
await mock()
Ответ №4:
Подклассы MagicMock
будут распространять ваш пользовательский класс для всех макетов, созданных на основе вашего макета сопрограммы. Например, AsyncMock().__str__
также станет AsyncMock
, что, вероятно, не то, что вы ищете.
Вместо этого вы можете определить фабрику, которая создает a Mock
(или a 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
Комментарии:
1. это не работает, side_effect=сопрограмма(coro), сопрограмма не определена
2. На самом деле мне немного больше нравится оригинальное решение, так как оно не требует специального переписывания тестовой функции. Есть ли преимущества у этого подхода по сравнению с тем, который показан в вопросе?
Ответ №5:
Другой способ издеваться над сопрограммой-сделать сопрограмму, которая возвращает насмешку. Таким образом, вы можете издеваться над сопрограммами, которые будут переданы в 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()
Ответ №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())
Ответ №7:
Вы можете установить return_value
асинхронный метод следующим образом:
mock = unittest.mock.MagicMock()
mock.your_async_method.return_value = task_from_result(your_return_value)
async def task_from_result(result):
return result
Вызывающий должен будет сделать await your_async_method(..)
то же самое, как если бы метод не был высмеян.
Ответ №8:
Я не понимаю, почему никто не упомянул о доступной опции по умолчанию. python предоставляет асинхронную версию MagicMock.
Вы можете прочитать больше об этом здесь. https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Асинхронный двигатель
В случае, если вы используете патч, вам также не нужно вносить никаких других изменений. При необходимости он автоматически заменит его функцией асинхронного макета. Подробнее читайте здесь https://docs.python.org/3/library/unittest.mock.html#patch