次のコードは、ImGoingToBeMocked
をMockオブジェクトに置き換えたため、TypeError: 'Mock' object is not iterable
のImBeingTested.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())
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()
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_with
、call_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)
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()など)が正しく動作することが期待されています。
非同期モックを自分で作成できます。
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__()
ダスティンの答えは、大多数のケースでおそらく正しいものです。コルーチンが複数の値を返す必要がある別の問題がありました。 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つの直接的な欠点は次のとおりです。
Mock
.side_effect
または.return_value
属性を使用して、わかりやすく読みやすくする方法が見つかりませんでした。ええと、ここには既にたくさんの答えがありますが、私は拡張バージョンの 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
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')
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()
asynctest を使用してCoroutineMock
をインポートするか、asynctest.mock.patch
を使用できます