web-dev-qa-db-fra.com

Comment tester le code asyncio Python 3.4?

Quel est le meilleur moyen d'écrire des tests unitaires pour le code à l'aide de la bibliothèque Python 3.4 asyncio? Supposons que je veuille tester un client TCP (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())

Lors de l'exécution de ce scénario de test avec le programme d'exécution de test par défaut, le test réussit toujours, car la méthode ne s'exécute que jusqu'à la première instruction yield from, après quoi elle est renvoyée avant l'exécution d'assertions. Cela provoque toujours des tests.

Existe-t-il un programme d'exécution de test prédéfini capable de gérer un code asynchrone comme celui-ci?

63
Marvin Killing

J'ai temporairement résolu le problème en utilisant un décorateur inspiré du gen_test de Tornado:

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

Comme l'a suggéré J.F. Sebastian, ce décorateur bloquera jusqu'à ce que la méthode de test coroutine soit terminée. Cela me permet d’écrire des cas de test comme ceci:

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

Cette solution manque probablement certains cas Edge.

Je pense qu'une installation comme celle-ci devrait être ajoutée à la bibliothèque standard de Python pour rendre les interactions asyncio et unittest plus pratiques.

46
Marvin Killing

async_test, suggéré par Marvin Killing, peut certainement aider - ainsi que l'appel direct loop.run_until_complete()

Mais je recommande également fortement de recréer une nouvelle boucle d'événements pour chaque test et de passer directement de la boucle aux appels d'API (au moins asyncio accepte lui-même le paramètre loop avec mot clé uniquement pour chaque appel qui en a besoin).

Comme

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

qui isole les tests en cas de test et évite des erreurs étranges telles qu'une coroutine de longue durée créée dans test_a mais terminée uniquement à la date d'exécution test_b.

45
Andrew Svetlov

pytest-asyncio semble prometteur:

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

Utilisez cette classe au lieu de la classe de base unittest.TestCase:

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)
5
pylover

Vraiment, comme le async_test mentionné dans https://stackoverflow.com/a/23036785/350195 , voici une version mise à jour pour 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())
5
peralmq

Vous pouvez également utiliser aiounittest qui adopte une approche similaire à celle de @Andrew Svetlov, @Marvin Killing et envelopper dans la classe AsyncTestCase facile à utiliser:

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)

Comme vous pouvez le constater, le cas asynchrone est géré par AsyncTestCase. Il supporte également le test synchrone. Il est possible de fournir une boucle d'événement personnalisée, il suffit de remplacer AsyncTestCase.get_event_loop.

Si vous préférez (pour une raison quelconque) l’autre classe TestCase (par exemple, unittest.TestCase), vous pouvez utiliser async_test décorator:

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)
2
kwarunek

En général, je définis mes tests asynchrones comme des coroutines et j'utilise un décorateur pour les "synchroniser":

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())
0
jcazor