Python 3.4 asyncio 코드를 테스트하는 방법은 무엇입니까?


79

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명령 까지만 실행되고 그 후에는 어설 션을 실행하기 전에 반환 되므로 테스트는 항상 성공 합니다. 이로 인해 테스트가 항상 성공합니다.

이와 같은 비동기 코드를 처리 할 수있는 미리 빌드 된 테스트 실행기가 있습니까?


3
loop.run_until_complete()대신 사용할 수 있습니다 yield from. 을 (를) 참조하십시오 asyncio.test_utils.
jfs 2014

파이썬 3.5 이상 async defawait구문은 다음을 참조하십시오 : stackoverflow.com/questions/41263988/…
Udi

답변:


50

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

JF Sebastian이 제안한 것처럼이 데코레이터는 테스트 메소드 코 루틴이 완료 될 때까지 차단됩니다. 이를 통해 다음과 같은 테스트 케이스를 작성할 수 있습니다.

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

이 솔루션은 아마도 일부 엣지 케이스를 놓칠 수 있습니다.

나는이 같은 시설이 파이썬의 표준 수 있도록 라이브러리에 추가해야한다고 생각 asyncio하고 unittest상호 작용 상자보다 편리 밖으로.


데코레이터가 스레드 기본 루프가 아닌 특정 루프를 사용하도록이 솔루션을 수정하는 방법이 있습니까?
Sebastian

예, 함수 주석은 Python에서 인수를 사용할 수 있으므로 여기에서 이벤트 루프를 전달할 수 있습니다. 참고 인수를 쓰기 주석는 것을 liitle 처음에는 혼란 : stackoverflow.com/a/5929165/823869
잭 오코너

@ JackO'Connor 생각하면 평균 기능 장식 하지 기능 주석 기능과 같은 주석 파이썬의 의미를 특정 있습니다 docs.python.org/3/tutorial/...
더스틴 와이어트

나는 문제를 만났고 asyncio.get_event_loop()사용했습니다asyncio.new_event_loop()
James

asyncio.coroutine더 이상 사용되지 않으며 py3.10에서 제거 될 경고 : docs.python.org/3/library/…
metaperture

48

async_test, Marvin Killing이 제안한, 확실히 도움이 될 수 있습니다. 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)
balu

11
글쎄, 그건 그냥 내 습관이야. asyncio 및 / 또는 aio 기반 라이브러리에서 작업 할 때 asyncio.set_event_loop(None)라이브러리가 전역 루프 존재를 중계하지 않아야하고 명시 적 루프 전달로 안전하게 작업해야한다는 사실을 직접 지정하는 데 사용 합니다. asyncio 테스트 자체를위한 코드 스타일이며 내 라이브러리에서도 사용합니다.
Andrew Svetlov 2014 년

이 예제도 조롱 asyncio.open_connection해야하지 않습니까? 그것은 생산 실행ConnectionRefusedError: [Errno 61] Connect call failed ('127.0.0.1', 8888)
terrycojones

@terrycojones mock이 항상 필요한 것은 아닙니다. 예를 들어 로컬 주소를 사용하므로 테스트 실행 전 또는 setUp방법 에서 주소에 테스트 서버를 설정할 수 있습니다 . 구체적인 구현은 필요에 따라 다릅니다.
Andrew Svetlov 2015

더 많은 boilterplate를 제자리에 추가하지만 확실히 이것은 테스트를 단일화하고 분리하는 방법입니다
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
말콤

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
James

또한 mock.patch를 통한 모의 클래스가 작동을 멈 춥니 다. github.com/pytest-dev/pytest-asyncio/issues/42
Deviacium

15

https://stackoverflow.com/a/23036785/350195 에서 async_test언급 된 래퍼 와 정말 비슷합니다 . 여기에 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테스트 케이스의 autodiscoverable 만들기
patricksurry

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)

편집 1 :

중첩 테스트에 대한 @Nitay 답변을 참고하십시오 .


1
이것은 훌륭한 솔루션입니다. 여기에 약간의 변경 사항이 추가되었습니다. stackoverflow.com/a/60986764/328059
Nitay

1
코드에 설명을 추가하십시오. 코드만으로는 답이 아닙니다.
buhtz

5

당신은 또한 사용할 수 있습니다 aiounittest그 답을 죽이는 @Marvin, 앤드류 Svetlov와 유사한 접근 방식을 취하고 및 사용에 쉽게에서 포장 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__방법.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.