簡體   English   中英

如何使用夾具使 pytest 中的異步測試超時?

[英]How to timeout an async test in pytest with fixture?

我正在測試可能會死鎖的異步 function。 我試圖添加一個夾具來限制 function 在引發故障之前只運行 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

--

編輯:Mikhail 的解決方案工作正常。 不過,我找不到將它合並到夾具中的方法。

使用超時限制功能(或代碼塊)的便捷方法是使用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) )。


我不知道如何創建紋理(如果你真的需要夾具),但上面的代碼可以提供起點。 也不確定是否有一種共同的方法來更好地實現目標。

有一種方法可以使用fixtures進行超時,只需要在conftest.py添加以下鈎子conftest.py

  • 任何以timeout為前綴的夾具都必須返回測試可以運行的秒數( int , float )。
  • 選擇最近的夾具wrt范圍。 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被某些鈎子重新定義,它肯定不起作用。

我只是喜歡Quimby 用超時標記測試的方法 這是我改進它的嘗試,使用pytest 標記

# tests/conftest.py
import asyncio


@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_pyfunc_call(pyfuncitem: pytest.Function):
    """
    Wrap all tests marked with pytest.mark.async_timeout with their specified timeout.
    """
    orig_obj = pyfuncitem.obj

    if marker := pyfuncitem.get_closest_marker("async_timeout"):

        async def new_obj(*args, **kwargs):
            """Timed wrapper around the test function."""
            try:
                return await asyncio.wait_for(orig_obj(*args, **kwargs), timeout=marker.args[0])
            except (asyncio.CancelledError, asyncio.TimeoutError):
                pytest.fail(f"Test {pyfuncitem.name} did not finish in time.")

        pyfuncitem.obj = new_obj

    yield


def pytest_configure(config: pytest.Config):
    config.addinivalue_line("markers", "async_timeout(timeout): cancels the test execution after the specified amount of seconds")

用法:

@pytest.mark.asyncio
@pytest.mark.async_timeout(10)
async def potentially_hanging_function():
    await asyncio.sleep(20)

將它包含到pytest-asyncio上的asyncio標記中應該不難,因此我們可以獲得如下語法:

@pytest.mark.asyncio(timeout=10)
async def potentially_hanging_function():
    await asyncio.sleep(20)

編輯:看起來已經有一個 PR

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM