tutorial loop asyncio async python unit-testing python-3.x python-unittest python-asyncio

loop - ¿Cómo probar el código de Python 3.4 asyncio?



task in python (7)

¿Cuál es la mejor manera de escribir pruebas unitarias para el código utilizando la biblioteca Python 3.4 asyncio ? Supongamos que quiero probar un cliente 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())

Al ejecutar este caso de prueba con el corredor de prueba predeterminado, la prueba siempre tendrá éxito ya que el método se ejecuta solo hasta el primer yield from instrucción, después de lo cual regresa antes de ejecutar cualquier aserciones. Esto hace que las pruebas siempre tengan éxito.

¿Hay un corredor de prueba precompilado que pueda manejar un código asíncrono como este?


Normalmente defino mis pruebas asíncronas como corutinas y uso un decorador para "sincronizarlas":

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


Realmente me gusta el contenedor async_test mencionado en https://.com/a/23036785/350195 , aquí hay una versión actualizada para 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())


También puedes usar aiounittest que adopta un enfoque similar al de @Andrew Svetlov, @Marvin Killing responde y lo envuelve en una clase AsyncTestCase fácil de usar:

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)

Como puede ver, AsyncTestCase maneja el caso asíncrono. También es compatible con la prueba síncrona. Existe la posibilidad de proporcionar un bucle de evento personalizado, simplemente anule AsyncTestCase.get_event_loop .

Si prefiere (por algún motivo) la otra clase TestCase (p. Ej. unittest.TestCase ), puede usar async_test decorator:

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)


Utilice esta clase en lugar de unittest.TestCase base class:

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)


gen_test temporalmente el problema usando un decorador inspirado en 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

Como sugirió JF Sebastian, este decorador se bloqueará hasta que el método de prueba coroutine haya terminado. Esto me permite escribir casos de prueba como este:

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

Esta solución probablemente omita algunos casos extremos.

Creo que una instalación como esta debe agregarse a la biblioteca estándar de Python para que la interacción asyncio y unittest más conveniente de manera asyncio .


pytest-asyncio parece prometedor:

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


async_test , sugerido por Marvin Killing, definitivamente puede ayudar, así como las llamadas directas loop.run_until_complete()

Pero también recomiendo encarecidamente recrear un nuevo ciclo de eventos para cada prueba y pasar directamente el ciclo a las llamadas API (al menos asyncio acepta el parámetro de palabra clave solo para cada llamada que lo necesite).

Me gusta

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

que aísla las pruebas en el caso de prueba y previene errores extraños como la corutina de larga data que se ha creado en test_a pero finalizó solo en el tiempo de ejecución de test_b .