web-dev-qa-db-ja.com

非同期コルーチンをモックする方法は?

次のコードは、ImGoingToBeMockedをMockオブジェクトに置き換えたため、TypeError: 'Mock' object is not iterableImBeingTested.i_call_other_coroutinesで失敗します。

コルーチンをモックするにはどうすればよいですか?

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())
22
Dustin Wyatt

mockライブラリはコルーチンをサポートしていないので、モックされたコルーチンを手動で作成し、モックオブジェクトに割り当てます。もう少し冗長ですが、機能します。

あなたの例は次のようになります:

import asyncio
import unittest
from unittest.mock import 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)
        ibt = ImBeingTested(mocked)

        @asyncio.coroutine
        def mock_coro():
            return "sup"
        mocked.yeah_im_not_going_to_run = mock_coro

        ret = asyncio.get_event_loop().run_until_complete(
            ibt.i_call_other_coroutines())
        self.assertEqual("sup", ret)


if __name__ == '__main__':
    unittest.main()
17
Andrew Svetlov

Andrew Svetlovの 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)
15
Dustin Wyatt

Asyncioのテストを作成する際にボイラープレートをカットすることを目的としたunittestのラッパーを作成しています。

コードはここにあります: 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属性でも機能し、spec付きのasynctest.MockはCoroutineMockを返すことができます。

>>> asyncio.iscoroutinefunction(Foo().coroutine)
True
>>> asyncio.iscoroutinefunction(Foo().function)
False
>>> asynctest.Mock(spec=Foo()).coroutine
<class 'asynctest.mock.CoroutineMock'>
>>> asynctest.Mock(spec=Foo()).function
<class 'asynctest.mock.Mock'>

Unittest.Mockのすべての機能(patch()など)が正しく動作することが期待されています。

11
Martin Richard

非同期モックを自分で作成できます。

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__()
5
e-satis

ダスティンの答えは、大多数のケースでおそらく正しいものです。コルーチンが複数の値を返す必要がある別の問題がありました。 my comment で簡単に説明したように、read()操作をシミュレートします。

さらにテストを行った後、モック関数の外部にイテレーターを定義し、次のコードを送信するために返された最後の値を効果的に記憶することで、以下のコードが機能しました。

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)

このアプローチで見られる2つの直接的な欠点は次のとおりです。

  1. 例外を簡単に発生させることはできません(たとえば、最初に一部のデータを返し、次に2番目の読み取り操作でエラーを発生させます)。
  2. 標準のMock.side_effectまたは.return_value属性を使用して、わかりやすく読みやすくする方法が見つかりませんでした。
2
AlexandreH

ええと、ここには既にたくさんの答えがありますが、私は拡張バージョンの e-satisの答え を寄稿します。このクラスは、Mockクラスが同期関数に対して行うのと同じように、非同期関数をモックし、呼び出しカウントと呼び出し引数を追跡します。

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
1
Mark E. Haase

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')
0
augurar

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()
0
David Parks

asynctest を使用してCoroutineMockをインポートするか、asynctest.mock.patchを使用できます

0
Natim