Как проверить код асинхронного кода Python 3.4?
Какой лучший способ написать модульные тесты для кода с использованием библиотеки Python 3.4 asyncio
? Предположим, что я хочу протестировать клиент 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())
При запуске этого тестового примера со стандартным тестовым бегуном тест всегда будет успешным, так как метод выполняется только до первой инструкции yield from
, после чего он возвращается до выполнения любых утверждений. Это заставляет тесты всегда преуспеть.
Есть ли готовый тестовый бегун, который способен обрабатывать асинхронный код, подобный этому?
Ответы
Ответ 1
async_test
, предложенный Марвином Киллинг, определенно может помочь - а также прямой вызов loop.run_until_complete()
Но я также настоятельно рекомендую воссоздать новый цикл событий для каждого теста и напрямую передать цикл на вызовы API (по крайней мере asyncio
сам принимает loop
параметр только для ключевого слова для каждого требующего его вызова).
Как
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())
который изолирует тесты в тестовом примере и предотвращает появление странных ошибок, таких как длительный сопроцессор, который был создан в test_a
, но завершен только при test_b
времени выполнения.
Ответ 2
Я временно решил проблему, используя декоратор, вдохновленный Tornado gen_test:
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
Подобно предложению Ю.Ф. Себастьяна, этот декоратор будет блокироваться до тех пор, пока не будет завершен показ метода coroutine. Это позволяет мне писать тестовые примеры следующим образом:
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())
Это решение, вероятно, пропускает некоторые случаи краев.
Я думаю, что подобное средство должно быть добавлено в стандартную библиотеку Python, чтобы сделать asyncio
и unittest
взаимодействие более удобным из коробки.
Ответ 3
pytest-asyncio выглядит многообещающим:
@pytest.mark.asyncio
async def test_some_asyncio_code():
res = await library.do_something()
assert b'expected result' == res
Ответ 4
Используйте этот класс вместо 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
Вы также можете использовать aiounittest
, который использует аналогичный подход, поскольку @Andrew Svetlov, @Marvin Killing отвечает и обертывает его простым в использовании AsyncTestCase
класс:
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)
Как вы можете видеть, асинхронный случай обрабатывается AsyncTestCase
. Он поддерживает также синхронный тест. Существует возможность создать настраиваемый цикл событий, просто переопределить AsyncTestCase.get_event_loop
.
Если вы предпочитаете (по какой-либо причине) другой класс TestCase (например, unittest.TestCase
), вы можете использовать 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)
Ответ 6
На самом деле, как обертка async_test
, упомянутая в fooobar.com/questions/103060/..., приведена обновленная версия для 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())
Ответ 7
Обычно я определяю свои асинхронные тесты как сопрограммы и использую декоратор для их синхронизации:
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())