web-dev-qa-db-ja.com

python 3.5での非同期呼び出しのモック

unittest.mock.patchを使用して、1つのネイティブコルーチンから別のコルーチンに非同期呼び出しをモックするにはどうすればよいですか?

私は現在非常に厄介なソリューションを持っています:

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)

これは機能しますが、見苦しいです。これを行うためのPython的な方法はありますか?

30
Zozz

MagicMockをサブクラス化すると、コルーチンモックから生成されたすべてのモックのカスタムクラスが伝播されます。たとえば、AsyncMock().__str__AsyncMockになりますが、これはおそらく探しているものではありません。

代わりに、カスタム引数を持つMock(またはMagicMock)を作成するファクトリーを定義したい場合があります(例:side_effect=coroutine(coro))。また、コルーチン関数をコルーチンから分離することをお勧めします( documentation で説明されています)。

ここに私が思いついたものがあります:

_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
_
11
Vincent

おそらく最も簡単で明確な解決策は誰もが見逃しています:

@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)

コルーチンは、未来を返すことが保証されている関数であると考えることができ、その関数は待たされる可能性があることを忘れないでください。

34
SColvin

解決策は実際には非常に簡単でした。変換する必要があったのは__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
33
Zozz

@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

コルーチンをモックするもう1つの方法は、モルーチンを返すコルーチンを作成することです。この方法で、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
Zozz

非同期オブジェクトをモックするための「最も簡単な」ソリューションのもう1つのバリエーションは、1つのライナーです。

ソース内:

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())
3
Murphy Meng