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?
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.
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
.
pytest-asyncio semble prometteur:
@pytest.mark.asyncio
async def test_some_asyncio_code():
res = await library.do_something()
assert b'expected result' == res
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)
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())
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)
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())