Издевательский асинхронный вызов в python 3.5

#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