Як протестувати Python 3.4 asyncio код?


79

Який найкращий спосіб написати модульні тести для коду за допомогою asyncioбібліотеки Python 3.4 ? Припустимо, я хочу протестувати 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інструкції, після чого він повертається перед виконанням будь-яких тверджень. Це змушує тести завжди бути успішними.

Чи існує заздалегідь побудований тест-драйвер, який здатний обробляти такий асинхронний код?


3
ви можете використовувати loop.run_until_complete()замість yield from. Дивіться також asyncio.test_utils.
jfs

Щодо python 3.5+ async defта awaitсинтаксису, див .: stackoverflow.com/questions/41263988/…
Udi

Відповіді:


50

Я тимчасово вирішив проблему за допомогою декоратора, натхненного 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

Як запропонував Дж. Ф. Себастьян, цей декоратор буде блокувати, доки не закінчиться корутина методу випробування. Це дозволяє мені писати такі тестові приклади:

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взаємодія більш зручним з коробки.


Чи є спосіб змінити це рішення, щоб декоратор використовував певний цикл, а не цикл за замовчуванням потоків?
Себастьян

Так, анотації функцій можуть приймати аргументи в Python, тому ви можете передати там цикл подій. Зверніть увагу, що написання анотацій, які беруть аргументи, спочатку мало заплутане: stackoverflow.com/a/5929165/823869
Джек О'Коннор,

@ JackO'Connor Я думаю , що ви маєте в виду функції декоратори не функція анотацій як функції анотації має певне значення в Python: docs.python.org/3/tutorial / ...
Дастін Wyatt

Я зіткнувся з проблемами asyncio.get_event_loop()та використовував їхasyncio.new_event_loop()
Джеймс

Попередження, яке asyncio.coroutineзастаріло і буде видалено в py3.10: docs.python.org/3/library/…
metaperture

48

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
Чи є якась причина, чому ви це робите, asyncio.set_event_loop(None)а пізніше self.loopявно переходите до asyncio.open_connection()цього, замість того, щоб просто робити це asyncio.set_event_loop(self.loop)з самого початку?
балу

11
Ну, це просто моя звичка. Коли я працюю над бібліотеками asyncio та / або aio, я asyncio.set_event_loop(None)безпосередньо вказую той факт, що бібліотека не повинна спиратися на існування глобального циклу і безпечно працювати шляхом явного передавання циклу. Це стиль коду для самих тестів asyncio, я теж використовую його у своїх бібліотеках.
Андрій Свєтлов

Цей приклад також asyncio.open_connectionповинен глузувати, чи не так? Запуск він виробляєConnectionRefusedError: [Errno 61] Connect call failed ('127.0.0.1', 8888)
terrycojones

Знущання над @terrycojones вимагається не завжди. У прикладі я використовую локальну адресу, тому я можу налаштувати тестовий сервер на адресу перед тестовим запуском або setUpметодом. Конкретна реалізація залежить від ваших потреб.
Андрій Свєтлов,

Додає більше попередньо встановлених пластин, але остаточно це спосіб зробити тести унітарними та ізольованими
danius

42

Оскільки Python 3.8 unittest постачається з функцією IsolatedAsyncioTestCase , призначеною для цієї мети.

from unittest import IsolatedAsyncioTestCase

class Test(IsolatedAsyncioTestCase):

    async def test_functionality(self):
        result = await functionality()
        self.assertEqual(expected, result)

1
Шкода, що ця відповідь показана після не менш ніж 5 обхідних шляхів лише на сьогодні.
konstantin

1
@Marvin Killing міг прийняти цю відповідь, і це, можливо, змінило б ...
Малкольм

16

pytest-asyncio виглядає багатообіцяючим:

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

1
Існує проблема з підходом pytest при використанні unittest.TestCase, який робить для мене дуже обмеженим. jacobbridges.github.io/post/unit-testing-with-asyncio
kwarunek

Схоже, тут було подано питання до них. Поки немає рішення. github.com/pytest-dev/pytest-asyncio/issues/15
Джеймс

Також знущання над класами через mock.patch перестає працювати. github.com/pytest-dev/pytest-asyncio/issues/42
Deviacium

15

Дійсно, як async_testобгортка, згадана в https://stackoverflow.com/a/23036785/350195 , ось оновлена ​​версія для Python 3.5+

def async_test(coro):
    def wrapper(*args, **kwargs):
        loop = asyncio.new_event_loop()
        try:
            return loop.run_until_complete(coro(*args, **kwargs))
        finally:
            loop.close()
    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())

1
для тих, хто використовує nosetests, можливо, ви захочете перейменувати декоратор або ніс вважає, що це насправді також тест, із загадковим повідомленням про async_testвідсутність необхідного позиційного аргументу. Я перейменовував asynctestі додав додатковий декоратор, @nose.tools.istestщоб зробити тест-кейс
автовідкритим

Обгортка async_testз nose.tools.nottestдекоратором при використанні nosetests.
millerdev

9

Використовуйте цей клас замість 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)

EDIT 1:

Зверніть увагу на відповідь @Nitay про вкладені тести.


1
Це чудове рішення. Додано в невелика зміна тут: stackoverflow.com/a/60986764/328059
Nitay

1
Будь ласка, додайте опис до свого коду. Лише код не є відповіддю.
buhtz

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декоратор:

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)

1

Я зазвичай визначаю свої асинхронні тести як спільні програми і використовую декоратор для їх "синхронізації":

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

1

Відповідь на pylover правильна і це те, що слід додати до unittest IMO.

Я додав би невелику зміну для підтримки вкладених асинхронних тестів:

class TestCaseBase(unittest.TestCase):
    # noinspection PyPep8Naming
    def __init__(self, methodName='runTest', loop=None):
        self.loop = loop or asyncio.get_event_loop()
        self._function_cache = {}
        super(BasicRequests, self).__init__(methodName=methodName)

    def coroutine_function_decorator(self, func):
        def wrapper(*args, **kw):
            # Is the io loop is already running? (i.e. nested async tests)
            if self.loop.is_running():
                t = func(*args, **kw)
            else:
                # Nope, we are the first
                t = self.loop.run_until_complete(func(*args, **kw))
            return t

        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

0

На додаток до відповіді pylover, якщо ви маєте намір використовувати якийсь інший асинхронний метод із самого тестового класу, наступна реалізація буде працювати краще -

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) and item.startswith('test_'):
            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 multiplier(self, n):
        await asyncio.sleep(1)  # just to show the difference
        return n*2

    async def test_dispatch(self):
        m = await self.multiplier(2)
        self.assertEqual(m, 4)

єдина зміна була - and item.startswith('test_')у __getattribute__методі.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.