Der folgende Code schlägt mit TypeError: 'Mock' object is not iterable
in ImBeingTested.i_call_other_coroutines
fehl, weil ich ImGoingToBeMocked
durch ein Mock-Objekt ersetzt habe.
Wie kann ich Coroutinen verspotten?
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())
Da die Bibliothek mock
keine Coroutinen unterstützt, erstelle ich verspottete Coroutinen manuell und weise diese dem verspotteten Objekt zu. Ein bisschen ausführlicher, aber es funktioniert.
Ihr Beispiel könnte so aussehen:
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 __== '__main__':
unittest.main()
Aus Andrew Svetlovs Antwort entsprungen, wollte ich nur diese Hilfsfunktion teilen:
def get_mock_coro(return_value):
@asyncio.coroutine
def mock_coro(*args, **kwargs):
return return_value
return Mock(wraps=mock_coro)
Auf diese Weise können Sie den Standard-assert_called_with
, call_count
und andere Methoden und Attribute verwenden, die Ihnen ein regulärer unittest.Mock bietet.
Sie können dies mit Code in der folgenden Frage verwenden:
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)
Ich schreibe einen Wrapper für unittest, der darauf abzielt, die Kesselplatte zu zerschneiden, wenn Tests für Asyncio geschrieben werden.
Der Code lebt hier: https://github.com/Martiusweb/asynctest
Sie können eine Coroutine mit asynctest.CoroutineMock
verspotten:
>>> mock = CoroutineMock(return_value='a result')
>>> asyncio.iscoroutinefunction(mock)
True
>>> asyncio.iscoroutine(mock())
True
>>> asyncio.run_until_complete(mock())
'a result'
Es funktioniert auch mit dem Attribut side_effect
, und ein asynctest.Mock
mit einem spec
kann CoroutineMock zurückgeben:
>>> 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'>
Es wird erwartet, dass alle Funktionen von unittest.Mock ordnungsgemäß funktionieren (patch () usw.).
Sie können selbst asynchrone Mocks erstellen:
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__()
Dustins Antwort ist wahrscheinlich in den allermeisten Fällen die richtige. Ich hatte ein anderes Problem, bei dem die Coroutine mehr als einen Wert zurückgeben musste, z. Simulation einer read()
-Operation, wie in meinem Kommentar kurz beschrieben.
Nach einigen weiteren Tests funktionierte der folgende Code für mich, indem er einen Iterator außerhalb der Verspottungsfunktion definierte und sich effektiv an den letzten Wert erinnerte, der zurückgegeben wurde, um den nächsten zu senden:
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.
Wenn wir also Dustins Antwort erweitern, würde es so aussehen:
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)
Die beiden unmittelbaren Nachteile, die ich bei diesem Ansatz sehe, sind:
Mock
.side_effect
oder .return_value
zu verwenden, um sie offensichtlicher und lesbarer zu machen.Sie können Mock
in Unterklassen unterteilen, um sich wie eine Coroutine-Funktion zu verhalten:
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)
Sie können CoroMock
so ziemlich wie einen normalen Mock verwenden, mit der Einschränkung, dass Anrufe erst aufgezeichnet werden, wenn die Coroutine von einer Ereignisschleife ausgeführt wird.
Wenn Sie ein Scheinobjekt haben und eine bestimmte Methode zu einer Koroutine machen möchten, können Sie Mock.attach_mock
so was:
mock.attach_mock(CoroMock(), 'method_name')
Ein leicht vereinfachtes Beispiel für python 3.6+, angepasst aus ein paar Antworten hier:
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()
Sie können asynchrone verwenden und CoroutineMock
importieren oder asynctest.mock.patch
Nun, hier gibt es bereits eine Reihe von Antworten, aber ich werde meine erweiterte Version von e-satis's answer beisteuern. Diese Klasse spottet eine asynchrone Funktion und verfolgt die Anzahl der Anrufe und die Anrufargumente, genau wie die Mock-Klasse für Synchronisierungsfunktionen.
Getestet auf 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
Beispielverwendung:
async def test_async_mock():
foo = AsyncMock(return_values=(1,2,3))
assert await foo() == 1
assert await foo() == 2
assert await foo() == 3