Wie teste ich Python 3.4 Asyncio Code?


78

Was ist der beste Weg, um Unit-Tests für Code mithilfe der Python 3.4- asyncioBibliothek zu schreiben ? Angenommen, ich möchte einen TCP-Client testen ( SocketConnection):

import asyncio
import unittest

class TestSocketConnection(unittest.TestCase):
    def setUp(self):
        self.mock_server = MockServer("localhost", 1337)
        self.socket_connection = SocketConnection("localhost", 1337)

    @asyncio.coroutine
    def test_sends_handshake_after_connect(self):
        yield from self.socket_connection.connect()
        self.assertTrue(self.mock_server.received_handshake())

Wenn Sie diesen Testfall mit dem Standardtestläufer ausführen, ist der Test immer erfolgreich, da die Methode nur bis zur ersten yield fromAnweisung ausgeführt wird. Danach wird sie zurückgegeben, bevor Zusicherungen ausgeführt werden. Dies führt dazu, dass Tests immer erfolgreich sind.

Gibt es einen vorgefertigten Testläufer, der so asynchronen Code verarbeiten kann?


3
Sie könnten loop.run_until_complete()anstelle von verwenden yield from. Siehe auch asyncio.test_utils.
JFS

Informationen zu Python 3.5+ async defund zur awaitSyntax finden Sie unter: stackoverflow.com/questions/41263988/…
Udi

Antworten:


50

Ich habe das Problem vorübergehend mit einem von Tornados gen_test inspirierten Dekorateur gelöst :

def async_test(f):
    def wrapper(*args, **kwargs):
        coro = asyncio.coroutine(f)
        future = coro(*args, **kwargs)
        loop = asyncio.get_event_loop()
        loop.run_until_complete(future)
    return wrapper

Wie von JF Sebastian vorgeschlagen, wird dieser Dekorateur blockieren, bis die Coroutine der Testmethode abgeschlossen ist. Dadurch kann ich Testfälle wie folgt schreiben:

class TestSocketConnection(unittest.TestCase):
    def setUp(self):
        self.mock_server = MockServer("localhost", 1337)
        self.socket_connection = SocketConnection("localhost", 1337)

    @async_test
    def test_sends_handshake_after_connect(self):
        yield from self.socket_connection.connect()
        self.assertTrue(self.mock_server.received_handshake())

Bei dieser Lösung fehlen wahrscheinlich einige Randfälle.

Ich denke , eine Einrichtung wie diese Python-Standardbibliothek hinzugefügt sollte zu machen asynciound unittestInteraktion bequemer aus dem Kasten heraus .


Gibt es eine Möglichkeit, diese Lösung so zu ändern, dass der Dekorateur eine bestimmte Schleife verwendet, nicht die Standardschleife des Threads?
Sebastian

Ja, Funktionsanmerkungen können in Python Argumente annehmen, sodass Sie dort eine Ereignisschleife übergeben können. Beachten Sie, dass das Schreiben von Anmerkungen, die Argumente enthalten , zunächst etwas verwirrend ist: stackoverflow.com/a/5929165/823869
Jack O'Connor

@ JackO'Connor Ich glaube , Sie mittlere Funktion Dekorateure nicht Funktion Anmerkungen als Funktion Anmerkungen hat eine spezifische in Python Bedeutung: docs.python.org/3/tutorial/...
Dustin Wyatt

Ich stieß auf Probleme mit asyncio.get_event_loop()und benutzteasyncio.new_event_loop()
James

Warnung, asyncio.coroutinedie veraltet ist und in py3.10 entfernt wird: docs.python.org/3/library/…
metaperture

48

async_test, vorgeschlagen von Marvin Killing, kann definitiv helfen - und auch direkt anrufen loop.run_until_complete()

Ich empfehle jedoch auch dringend, für jeden Test eine neue Ereignisschleife neu zu erstellen und die Schleife direkt an API-Aufrufe zu übergeben (zumindest asyncioakzeptiert sie selbst loopnur Schlüsselwortparameter für jeden Aufruf, der sie benötigt).

Mögen

class Test(unittest.TestCase):
    def setUp(self):
        self.loop = asyncio.new_event_loop()
        asyncio.set_event_loop(None)

    def test_xxx(self):
        @asyncio.coroutine
        def go():
            reader, writer = yield from asyncio.open_connection(
                '127.0.0.1', 8888, loop=self.loop)
            yield from asyncio.sleep(0.01, loop=self.loop)
        self.loop.run_until_complete(go())

Dies isoliert Tests im Testfall und verhindert seltsame Fehler wie langjährige Coroutine, die in erstellt, test_aaber erst zur test_bAusführungszeit beendet wurde.


2
Gibt es einen Grund, warum Sie dies tun asyncio.set_event_loop(None)und später self.loopexplizit an übergeben, asyncio.open_connection()anstatt es von asyncio.set_event_loop(self.loop)Anfang an richtig zu machen?
Balu

11
Nun, es ist nur meine Gewohnheit. Wenn ich an asynchronen und / oder aio-basierten Bibliotheken arbeite asyncio.set_event_loop(None), spezifiziere ich direkt die Tatsache, dass die Bibliothek nicht auf die Existenz einer globalen Schleife weitergeleitet werden soll und sicher durch explizite Schleifenübergabe arbeiten soll. Es ist der Codestil für Asyncio-Tests selbst, ich verwende ihn auch in meinen Bibliotheken.
Andrew Svetlov

Dieses Beispiel sollte sich auch verspotten, asyncio.open_connectionnicht wahr? Laufen es produziertConnectionRefusedError: [Errno 61] Connect call failed ('127.0.0.1', 8888)
Terrycojones

@terrycojones mock ist nicht immer erforderlich. In einem Beispiel verwende ich eine lokale Adresse, damit ich den Testserver vor dem Testlauf oder in der setUpMethode auf der Adresse einrichten kann. Die konkrete Umsetzung hängt von Ihren Anforderungen ab.
Andrew Svetlov

Fügt mehr Boilterplate hinzu, aber definitiv ist dies die Art und Weise, Tests einheitlich und isoliert zu machen
Danius

33

Da Python 3.8 unittest mit der für diesen Zweck entwickelten IsolatedAsyncioTestCase- Funktion geliefert wird .

from unittest import IsolatedAsyncioTestCase

class Test(IsolatedAsyncioTestCase):

    async def test_functionality(self):
        result = await functionality()
        self.assertEqual(expected, result)

Schade, dass diese Antwort erst ab heute nach nicht weniger als 5 Problemumgehungen angezeigt wird.
konstantin

16

pytest-asyncio sieht vielversprechend aus:

@pytest.mark.asyncio
async def test_some_asyncio_code():
    res = await library.do_something()
    assert b'expected result' == res

1
Es gibt ein Problem mit dem Pytest-Ansatz bei der Verwendung unittest.TestCase, das für mich sehr begrenzt ist. jacobbridges.github.io/post/unit-testing-with-asyncio
kwarunek

Es sieht so aus, als ob hier ein Problem bei ihnen eingereicht wurde. Noch keine Lösung. github.com/pytest-dev/pytest-asyncio/issues/15
James

Auch das Verspotten von Klassen über mock.patch funktioniert nicht mehr. github.com/pytest-dev/pytest-asyncio/issues/42
Deviacium

13

Genau wie der async_testin https://stackoverflow.com/a/23036785/350195 erwähnte Wrapper ist hier eine aktualisierte Version für Python 3.5+

def async_test(coro):
    def wrapper(*args, **kwargs):
        loop = asyncio.new_event_loop()
        return loop.run_until_complete(coro(*args, **kwargs))
    return wrapper



class TestSocketConnection(unittest.TestCase):
    def setUp(self):
        self.mock_server = MockServer("localhost", 1337)
        self.socket_connection = SocketConnection("localhost", 1337)

    @async_test
    async def test_sends_handshake_after_connect(self):
        await self.socket_connection.connect()
        self.assertTrue(self.mock_server.received_handshake())

1
Für jeden, der es verwendet nosetests, möchten Sie möglicherweise den Dekorateur umbenennen, oder die Nase denkt, dass es sich tatsächlich auch um einen Test handelt, mit einer mysteriösen Meldung über das async_testFehlen eines erforderlichen Positionsarguments. I umbenannt asynctestund eine zusätzliche Dekorateur hinzugefügt @nose.tools.istestden Testfall autodiscoverable zu machen
patricksurry

9

Verwenden Sie diese Klasse anstelle der unittest.TestCaseBasisklasse:

import asyncio
import unittest


class AioTestCase(unittest.TestCase):

    # noinspection PyPep8Naming
    def __init__(self, methodName='runTest', loop=None):
        self.loop = loop or asyncio.get_event_loop()
        self._function_cache = {}
        super(AioTestCase, self).__init__(methodName=methodName)

    def coroutine_function_decorator(self, func):
        def wrapper(*args, **kw):
            return self.loop.run_until_complete(func(*args, **kw))
        return wrapper

    def __getattribute__(self, item):
        attr = object.__getattribute__(self, item)
        if asyncio.iscoroutinefunction(attr):
            if item not in self._function_cache:
                self._function_cache[item] = self.coroutine_function_decorator(attr)
            return self._function_cache[item]
        return attr


class TestMyCase(AioTestCase):

    async def test_dispatch(self):
        self.assertEqual(1, 1)

EDIT 1:

Bitte beachten Sie die @ Nitay- Antwort zu verschachtelten Tests.


1
Dies ist eine großartige Lösung. In einer kleinen Änderung hier hinzugefügt
Nitay

1
Bitte fügen Sie Ihrem Code eine Beschreibung hinzu. Nur Code ist keine Antwort.
22.

5

Sie können auch einen aiounittestähnlichen Ansatz verwenden wie @Andrew Svetlov, @Marvin Killing antwortet und ihn in eine benutzerfreundliche AsyncTestCaseKlasse einwickelt :

import asyncio
import aiounittest


async def add(x, y):
    await asyncio.sleep(0.1)
    return x + y

class MyTest(aiounittest.AsyncTestCase):

    async def test_async_add(self):
        ret = await add(5, 6)
        self.assertEqual(ret, 11)

    # or 3.4 way
    @asyncio.coroutine
    def test_sleep(self):
        ret = yield from add(5, 6)
        self.assertEqual(ret, 11)

    # some regular test code
    def test_something(self):
        self.assertTrue(true)

Wie Sie sehen, wird der asynchrone Fall von behandelt AsyncTestCase. Es unterstützt auch synchronen Test. Es besteht die Möglichkeit, eine benutzerdefinierte Ereignisschleife bereitzustellen, die einfach überschrieben wird AsyncTestCase.get_event_loop.

Wenn Sie (aus irgendeinem Grund) die andere TestCase-Klasse bevorzugen (z. B. unittest.TestCase), können Sie async_testDecorator verwenden:

import asyncio
import unittest
from aiounittest import async_test


async def add(x, y):
    await asyncio.sleep(0.1)
    return x + y

class MyTest(unittest.TestCase):

    @async_test
    async def test_async_add(self):
        ret = await add(5, 6)
        self.assertEqual(ret, 11)

1

Normalerweise definiere ich meine asynchronen Tests als Coroutinen und verwende einen Dekorator, um sie zu "synchronisieren":

import asyncio
import unittest

def sync(coro):
    def wrapper(*args, **kwargs):
        loop = asyncio.get_event_loop()
        loop.run_until_complete(coro(*args, **kwargs))
    return wrapper

class TestSocketConnection(unittest.TestCase):
    def setUp(self):
        self.mock_server = MockServer("localhost", 1337)
        self.socket_connection = SocketConnection("localhost", 1337)

    @sync
    async def test_sends_handshake_after_connect(self):
        await self.socket_connection.connect()
        self.assertTrue(self.mock_server.received_handshake())

1

Die Pylover-Antwort ist korrekt und sollte zu unittest IMO hinzugefügt werden.

Ich würde eine kleine Änderung hinzufügen, um verschachtelte asynchrone Tests zu unterstützen:

class TestCaseBase(unittest.TestCase):
    # noinspection PyPep8Naming
    def __init__(self, methodName='runTest', loop=None):
        self.loop = loop or asyncio.get_event_loop()
        self._function_cache = {}
        super(BasicRequests, self).__init__(methodName=methodName)

    def coroutine_function_decorator(self, func):
        def wrapper(*args, **kw):
            # Is the io loop is already running? (i.e. nested async tests)
            if self.loop.is_running():
                t = func(*args, **kw)
            else:
                # Nope, we are the first
                t = self.loop.run_until_complete(func(*args, **kw))
            return t

        return wrapper

    def __getattribute__(self, item):
        attr = object.__getattribute__(self, item)
        if asyncio.iscoroutinefunction(attr):
            if item not in self._function_cache:
                self._function_cache[item] = self.coroutine_function_decorator(attr)
            return self._function_cache[item]
        return attr

0

Wenn Sie neben der Antwort von pylover eine andere asynchrone Methode aus der Testklasse selbst verwenden möchten, funktioniert die folgende Implementierung besser:

import asyncio
import unittest

class AioTestCase(unittest.TestCase):

    # noinspection PyPep8Naming
    def __init__(self, methodName='runTest', loop=None):
        self.loop = loop or asyncio.get_event_loop()
        self._function_cache = {}
        super(AioTestCase, self).__init__(methodName=methodName)

    def coroutine_function_decorator(self, func):
        def wrapper(*args, **kw):
            return self.loop.run_until_complete(func(*args, **kw))
        return wrapper

    def __getattribute__(self, item):
        attr = object.__getattribute__(self, item)
        if asyncio.iscoroutinefunction(attr) and item.startswith('test_'):
            if item not in self._function_cache:
                self._function_cache[item] = 
                    self.coroutine_function_decorator(attr)
            return self._function_cache[item]
        return attr


class TestMyCase(AioTestCase):

    async def multiplier(self, n):
        await asyncio.sleep(1)  # just to show the difference
        return n*2

    async def test_dispatch(self):
        m = await self.multiplier(2)
        self.assertEqual(m, 4)

Die einzige Änderung war - and item.startswith('test_')in der __getattribute__Methode.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.