Подтвердить что ты не робот

Как проверить код асинхронного кода 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, после чего он возвращается до выполнения любых утверждений. Это заставляет тесты всегда преуспеть.

Есть ли готовый тестовый бегун, который способен обрабатывать асинхронный код, подобный этому?

4b9b3361

Ответ 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())