web-dev-qa-db-ger.com

Wie man asyncio coroutines verspottet?

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

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()
12
Andrew Svetlov

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)
12
Dustin Wyatt

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 speckann 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.).

9
Martin Richard

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

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:

  1. Es ist nicht einfach möglich, Ausnahmen auszulösen (z. B. zuerst einige Daten zurückzugeben und dann beim zweiten Lesevorgang einen Fehler auszulösen).
  2. Ich habe keine Möglichkeit gefunden, die Standardattribute Mock.side_effect oder .return_value zu verwenden, um sie offensichtlicher und lesbarer zu machen.
2
AlexandreH

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

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

Sie können asynchrone verwenden und CoroutineMock importieren oder asynctest.mock.patch

0
Natim

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