【问题标题】:How to timeout an async test in pytest with fixture?如何使用夹具使 pytest 中的异步测试超时?
【发布时间】:2019-04-15 07:56:35
【问题描述】:

我正在测试一个可能会死锁的异步函数。我尝试添加一个固定装置以限制该功能在引发故障之前仅运行 5 秒,但到目前为止它还没有起作用。

设置:

pipenv --python==3.6
pipenv install pytest==4.4.1
pipenv install pytest-asyncio==0.10.0

代码:

import asyncio
import pytest

@pytest.fixture
def my_fixture():
  # attempt to start a timer that will stop the test somehow
  asyncio.ensure_future(time_limit())
  yield 'eggs'


async def time_limit():
  await asyncio.sleep(5)
  print('time limit reached')     # this isn't printed
  raise AssertionError


@pytest.mark.asyncio
async def test(my_fixture):
  assert my_fixture == 'eggs'
  await asyncio.sleep(10)
  print('this should not print')  # this is printed
  assert 0

--

编辑:米哈伊尔的解决方案工作正常。不过,我找不到将其合并到固定装置中的方法。

【问题讨论】:

  • 你不能在 pytest-asyncio 的夹具中等待测试。恐怕米哈伊尔的回答是唯一的解决方案。好问题。
  • 在我的测试中,我使用the following hook 添加所需的功能。对我来说效果很好。

标签: python pytest python-asyncio pytest-asyncio


【解决方案1】:

使用超时限制功能(或代码块)的便捷方法是使用async-timeout 模块。你可以在你的测试函数中使用它,或者,例如,创建一个装饰器。与夹具不同,它允许为每个测试指定具体时间:

import asyncio
import pytest
from async_timeout import timeout


def with_timeout(t):
    def wrapper(corofunc):
        async def run(*args, **kwargs):
            with timeout(t):
                return await corofunc(*args, **kwargs)
        return run       
    return wrapper


@pytest.mark.asyncio
@with_timeout(2)
async def test_sleep_1():
    await asyncio.sleep(1)
    assert 1 == 1


@pytest.mark.asyncio
@with_timeout(2)
async def test_sleep_3():
    await asyncio.sleep(3)
    assert 1 == 1

为具体时间创建装饰器并不难 (with_timeout_5 = partial(with_timeout, 5))。


我不知道如何创建纹理(如果你真的需要夹具),但上面的代码可以提供起点。也不确定是否有更好地实现目标的通用方法。

【讨论】:

  • 这行得通,但它不是一个夹具,它更适合 pytest 系统,并且可以自动应用于包中的每个测试函数。我想我现在会在我的代码中使用这种方法。我将等待几天,然后将其标记为已接受。
【解决方案2】:

有一种方法可以使用fixtures进行超时,只需将以下钩子添加到conftest.py中即可。

  • 任何以timeout 为前缀的fixture 必须返回测试可以运行的秒数(intfloat)。
  • 选择最接近的夹具 w.r.t 范围。 autouse 固定装置的优先级低于明确选择的固定装置。后一种是首选。不幸的是,函数参数列表中的顺序并不重要。
  • 如果没有这样的夹具,则测试不受限制,将像往常一样无限期地运行。
  • 测试也必须标有pytest.mark.asyncio,但无论如何都需要。
# Add to conftest.py
import asyncio

import pytest

_TIMEOUT_FIXTURE_PREFIX = "timeout"


@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_setup(item: pytest.Item):
    """Wrap all tests marked with pytest.mark.asyncio with their specified timeout.

    Must run as early as possible.

    Parameters
    ----------
    item : pytest.Item
        Test to wrap
    """
    yield
    orig_obj = item.obj
    timeouts = [n for n in item.funcargs if n.startswith(_TIMEOUT_FIXTURE_PREFIX)]
    # Picks the closest timeout fixture if there are multiple
    tname = None if len(timeouts) == 0 else timeouts[-1]

    # Only pick marked functions
    if item.get_closest_marker("asyncio") is not None and tname is not None:

        async def new_obj(*args, **kwargs):
            """Timed wrapper around the test function."""
            try:
                return await asyncio.wait_for(
                    orig_obj(*args, **kwargs), timeout=item.funcargs[tname]
                )
            except Exception as e:
                pytest.fail(f"Test {item.name} did not finish in time.")

        item.obj = new_obj

示例:

@pytest.fixture
def timeout_2s():
    return 2


@pytest.fixture(scope="module", autouse=True)
def timeout_5s():
    # You can do whatever you need here, just return/yield a number
    return 5


async def test_timeout_1():
    # Uses timeout_5s fixture by default
    await aio.sleep(0)  # Passes
    return 1


async def test_timeout_2(timeout_2s):
    # Uses timeout_2s because it is closest
    await aio.sleep(5)  # Timeouts

警告

可能不适用于其他一些插件,我只用pytest-asyncio测试过,如果item被某个钩子重新定义,它肯定不起作用。

【讨论】:

    猜你喜欢
    • 2021-12-29
    • 1970-01-01
    • 1970-01-01
    • 2018-02-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多