如何测试Python 3.4异步代码?


78

使用Python 3.4asyncio库编写代码的单元测试的最佳方法是什么?假设我要测试一个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年

对于Python 3.5+async defawait语法,请参阅:stackoverflow.com/questions/41263988/...
乌迪

Answers:


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

该解决方案可能会遗漏一些边缘情况。

我认为应该将这样的功能添加到Python的标准库中,以使开箱即用asynciounittest交互更加方便。


有没有办法修改此解决方案,以便装饰器使用特定的循环,而不是线程默认循环?
塞巴斯蒂安2014年

是的,函数注释可以在Python中接受参数,因此您可以在其中传递事件循环。请注意,编写带有参数的注释起初有点令人困惑:stackoverflow.com/a/5929165/823869
Jack O'Connor

@ JackO'Connor我认为您的意思是函数修饰符不是函数注解,因为函数注解在Python中具有特定含义: docs.python.org/3/tutorial/…–
Dustin Wyatt

我遇到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_atest_b执行时创建但仅在执行时完成的长期协程。


2
您是否有任何理由为什么要这样做,asyncio.set_event_loop(None)然后又self.loop明确地传递给您,asyncio.open_connection()而不是仅仅asyncio.set_event_loop(self.loop)从一开始就做呢?
2014年

11
好吧,这只是我的习惯。当我处理基于asyncio和/或基于aio的库时,我通常asyncio.set_event_loop(None)直接指定一个事实,即该库不应在全局循环存在时进行中继,而应通过显式循环传递安全地工作。它是异步测试本身的代码样式,我也在我的库中使用它。
安德鲁·斯维特洛夫

这个例子也应该模拟asyncio.open_connection吗?运行它会产生ConnectionRefusedError: [Errno 61] Connect call failed ('127.0.0.1', 8888)
terrycojones 2015年

并非总是需要@terrycojones模拟。在示例中,我使用本地地址,因此可以在测试运行之前或在setUp方法中在该地址上设置测试服务器。具体实施取决于您的需求。
安德鲁·斯维特洛夫

在适当的位置添加更多的样板,但最终这是使测试成为统一和隔离的方法
danius


16

pytest-asyncio看起来很有希望:

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

1
使用时unittest.TestCase,pytest方法存在问题,这对我来说非常有限。jacobbridges.github.io/post/unit-testing-with-asyncio
kwarunek

似乎在这里向他们提出了问题。尚无解决方案。github.com/pytest-dev/pytest-asyncio/issues/15-
詹姆斯(James)

通过嘲笑类通过嘲笑类也停止工作。github.com/pytest-dev/pytest-asyncio/issues/42
Deviacium

13

确实像https://stackoverflow.com/a/23036785/350195中async_test提到的包装器,这是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())

1
对于使用的任何人nosetests,您可能想重命名装饰器,或者鼻子认为它实际上也是一个测试,带有一条神秘的消息,提示async_test缺少必需的位置参数。我重命名为,asynctest并添加了一个附加的装饰器,@nose.tools.istest以使该测试用例可以自动发现
patricksurry

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
请在您的代码中添加描述。仅代码不是答案。
布兹

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答案是正确的,应该将其添加到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__法。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.