如何模拟asyncio协同程序?

时间:2015-04-26 17:51:08

标签: python unit-testing mocking python-asyncio

以下代码因TypeError: 'Mock' object is not iterable中的ImBeingTested.i_call_other_coroutines而失败,因为我已将Mock对象替换为ImGoingToBeMocked

我如何模仿协同程序?

class ImGoingToBeMocked:
    @asyncio.coroutine
    def yeah_im_not_going_to_run(self):
        yield from asyncio.sleep(1)
        return "sup"

class ImBeingTested:
    def __init__(self, hidude):
        self.hidude = hidude

    @asyncio.coroutine
    def i_call_other_coroutines(self):
        return (yield from self.hidude.yeah_im_not_going_to_run())

class TestImBeingTested(unittest.TestCase):

    def test_i_call_other_coroutines(self):
        mocked = Mock(ImGoingToBeMocked)
        ibt = ImBeingTested(mocked)

        ret = asyncio.get_event_loop().run_until_complete(ibt.i_call_other_coroutines())

9 个答案:

答案 0 :(得分:13)

由于<system.webServer> <httpProtocol> <customHeaders> <add name="X-Frame-Options" value="SAMEORIGIN" /> </customHeaders> </httpProtocol> </system.webServer> 库不支持协同程序,我手动创建模拟协程并将它们分配给模拟对象。更冗长但有效。

您的示例可能如下所示:

mock

答案 1 :(得分:12)

安德鲁·斯韦特洛夫的answer,我只是想分享这个辅助功能:

def get_mock_coro(return_value):
    @asyncio.coroutine
    def mock_coro(*args, **kwargs):
        return return_value

    return Mock(wraps=mock_coro)

这使您可以使用标准assert_called_withcall_count以及常规unittest.Mock为您提供的其他方法和属性。

您可以在以下问题中使用此代码:

class ImGoingToBeMocked:
    @asyncio.coroutine
    def yeah_im_not_going_to_run(self):
        yield from asyncio.sleep(1)
        return "sup"

class ImBeingTested:
    def __init__(self, hidude):
        self.hidude = hidude

    @asyncio.coroutine
    def i_call_other_coroutines(self):
        return (yield from self.hidude.yeah_im_not_going_to_run())

class TestImBeingTested(unittest.TestCase):

    def test_i_call_other_coroutines(self):
        mocked = Mock(ImGoingToBeMocked)
        mocked.yeah_im_not_going_to_run = get_mock_coro()
        ibt = ImBeingTested(mocked)

        ret = asyncio.get_event_loop().run_until_complete(ibt.i_call_other_coroutines())
        self.assertEqual(mocked.yeah_im_not_going_to_run.call_count, 1)

答案 2 :(得分:9)

我正在为unittest编写一个包装器,旨在在编写asyncio测试时削减样板。

代码位于此处:https://github.com/Martiusweb/asynctest

您可以使用asynctest.CoroutineMock模拟协程:

>>> mock = CoroutineMock(return_value='a result')
>>> asyncio.iscoroutinefunction(mock)
True
>>> asyncio.iscoroutine(mock())
True
>>> asyncio.run_until_complete(mock())
'a result'

它也适用于side_effect属性,带有asynctest.Mock的{​​{1}}可以返回CoroutineMock:

spec

unittest.Mock的所有功能都应该正常工作(patch()等)。

答案 3 :(得分:5)

您可以自己创建异步模拟:

import asyncio
from unittest.mock import Mock


class AsyncMock(Mock):

    def __call__(self, *args, **kwargs):
        sup = super(AsyncMock, self)
        async def coro():
            return sup.__call__(*args, **kwargs)
        return coro()

    def __await__(self):
        return self().__await__()

答案 4 :(得分:2)

在绝大多数情况下,达斯汀的答案可能是正确的答案。我有一个不同的问题,协程需要返回多个值,例如模拟read()操作,如我comment中所述。

经过一些测试后,下面的代码为我工作,通过在mocking函数之外定义一个迭代器,有效地记住返回的最后一个值以发送下一个:

def test_some_read_operation(self):
    #...
    data = iter([b'data', b''])
    @asyncio.coroutine
    def read(*args):
        return next(data)
    mocked.read = Mock(wraps=read)
    # Here, the business class would use its .read() method which
    # would first read 4 bytes of data, and then no data
    # on its second read.

因此,扩展达斯汀的答案,它看起来像:

def get_mock_coro(return_values):
    values = iter(return_values)
    @asyncio.coroutine
    def mock_coro(*args, **kwargs):
        return next(values)

    return Mock(wraps=mock_coro)

我在这种方法中可以看到的两个直接缺点是:

  1. 它不允许轻易引发异常(例如,首先返回一些数据,然后在第二次读取操作时引发错误)。
  2. 我没有找到使用标准Mock .side_effect.return_value属性的方法,以使其更加明显和可读。

答案 5 :(得分:1)

好吧,这里已经有很多答案了,但是我将贡献我的扩展版本e-satis's answer。此类模拟async函数并跟踪调用计数和调用args,就像Mock类对sync函数所做的一样。

在Python 3.7.0上进行了测试。

class AsyncMock:
    ''' A mock that acts like an async def function. '''
    def __init__(self, return_value=None, return_values=None):
        if return_values is not None:
            self._return_value = return_values
            self._index = 0
        else:
            self._return_value = return_value
            self._index = None
        self._call_count = 0
        self._call_args = None
        self._call_kwargs = None

    @property
    def call_args(self):
        return self._call_args

    @property
    def call_kwargs(self):
        return self._call_kwargs

    @property
    def called(self):
        return self._call_count > 0

    @property
    def call_count(self):
        return self._call_count

    async def __call__(self, *args, **kwargs):
        self._call_args = args
        self._call_kwargs = kwargs
        self._call_count += 1
        if self._index is not None:
            return_index = self._index
            self._index += 1
            return self._return_value[return_index]
        else:
            return self._return_value

用法示例:

async def test_async_mock():
    foo = AsyncMock(return_values=(1,2,3))
    assert await foo() == 1
    assert await foo() == 2
    assert await foo() == 3

答案 6 :(得分:0)

您可以使用asynctest并导入CoroutineMock或使用asynctest.mock.patch

答案 7 :(得分:0)

您可以将Mock子类化以充当协程函数:

class CoroMock(Mock):
    async def __call__(self, *args, **kwargs):
        return super(CoroMock, self).__call__(*args, **kwargs)

    def _get_child_mock(self, **kw):
        return Mock(**kw)

您可以像正常的模拟程序一样使用CoroMock,但要注意的是,只有在事件循环执行协程之后,才会记录调用。

如果您有一个模拟对象,并且想将特定方法设为协程,则可以像这样使用Mock.attach_mock

mock.attach_mock(CoroMock(), 'method_name')

答案 8 :(得分:0)

python 3.6+的一个略有简化的示例,改编自此处的一些答案:

import unittest

class MyUnittest()

  # your standard unittest function
  def test_myunittest(self):

    # define a local mock async function that does what you want, such as throw an exception. The signature should match the function you're mocking.
    async def mock_myasync_function():
      raise Exception('I am testing an exception within a coroutine here, do what you want')

    # patch the original function `myasync_function` with the one you just defined above, note the usage of `wrap`, which hasn't been used in other answers.
    with unittest.mock.patch('mymodule.MyClass.myasync_function', wraps=mock_myasync_function) as mock:
      with self.assertRaises(Exception):
        # call some complicated code that ultimately schedules your asyncio corotine mymodule.MyClass.myasync_function
        do_something_to_call_myasync_function()