【问题标题】:How to test Python 3.4 asyncio code?如何测试 Python 3.4 asyncio 代码?
【发布时间】:2014-05-26 20:51:37
【问题描述】:

使用 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 指令,之后它在执行任何断言之前返回。这会导致测试始终成功。

是否有预构建的测试运行器能够处理这样的异步代码?

【问题讨论】:

标签: python unit-testing python-3.x python-unittest python-asyncio


【解决方案1】:

由于 Python 3.8 unittest 带有 IsolatedAsyncioTestCase 函数,专为此目的而设计。

from unittest import IsolatedAsyncioTestCase

class Test(IsolatedAsyncioTestCase):

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

【讨论】:

  • 太糟糕了,直到今天,这个答案才在不少于 5 个解决方法后才显示出来。
  • @Marvin Killing 可以接受这个答案,这可能会改变......
  • 这是迄今为止最好的解决方案
  • 这确实应该被标记为接受的解决方案!
【解决方案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

就像 J.F. 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 的交互更加方便开箱即用。

【讨论】:

  • 有没有办法修改这个解决方案,让装饰器使用特定的循环,而不是线程默认循环?
  • 是的,函数注解可以在 Python 中接受参数,所以你可以在那里传递一个事件循环。请注意,编写带参数的注解一开始是liitle令人困惑的:stackoverflow.com/a/5929165/823869
  • @JackO'Connor 我认为你的意思是函数 decorators 而不是函数 annotations 因为函数 annotations 在蟒蛇:docs.python.org/3/tutorial/…
  • 我遇到了asyncio.get_event_loop() 的问题并使用了asyncio.new_event_loop()
  • 警告asyncio.coroutine 已弃用并将在py3.10 中删除:docs.python.org/3/library/…
【解决方案3】:

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 执行时间完成的长期协程。

【讨论】:

  • 你有什么理由做asyncio.set_event_loop(None)然后将self.loop明确传递给asyncio.open_connection(),而不是从一开始就做asyncio.set_event_loop(self.loop)
  • 嗯,这只是我的习惯。当我处理基于 asyncio 或 aio 的库时,我使用 asyncio.set_event_loop(None) 直接指定库不应依赖全局循环存在并通过显式循环传递安全地工作这一事实。它是异步测试本身的代码风格,我也在我的库中使用它。
  • 这个例子也应该模拟asyncio.open_connection,不是吗?运行它会产生ConnectionRefusedError: [Errno 61] Connect call failed ('127.0.0.1', 8888)
  • @terrycojones 模拟并不总是必需的。在示例中,我使用本地地址,因此我可以在测试运行之前在该地址上设置测试服务器,或者在setUp 方法中设置测试服务器。具体实施取决于您的需求。
  • 在适当的位置添加更多样板,但绝对是使测试统一和隔离的方式
【解决方案4】:

真的很像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())

【讨论】:

  • 对于使用nosetests 的任何人,您可能想要重命名装饰器,或者nose 认为这实际上也是一个测试,并带有一条关于async_test 缺少所需位置参数的神秘消息。我重命名为 asynctest 并添加了一个额外的装饰器 @nose.tools.istest 以使测试用例可自动发现
  • 如果使用nosetests,用nose.tools.nottest装饰器包裹async_test
  • 这很漂亮。加入我的项目。谢谢!
【解决方案5】:

pytest-asyncio 看起来很有希望:

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

【讨论】:

【解决方案6】:

使用这个类代替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 answer 关于嵌套测试。

【讨论】:

【解决方案7】:

您也可以使用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)

【讨论】:

    【解决方案8】:

    我通常将我的异步测试定义为协程并使用装饰器“同步”它们:

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

    【讨论】:

      【解决方案9】:

      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
      

      【讨论】:

        【解决方案10】:

        除了 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__ 方法中。

        【讨论】:

          猜你喜欢
          • 2018-02-10
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-04-21
          • 2023-03-22
          • 2015-06-22
          • 2023-03-04
          • 2017-06-28
          相关资源
          最近更新 更多