【问题标题】:Run tests concurrently同时运行测试
【发布时间】:2019-06-04 17:03:42
【问题描述】:

我想使用 asyncio (/curio/trio) 和 pytest 同时运行多个测试,但我找不到任何相关信息。我需要自己安排吗?如果我这样做了,有没有办法有一个很好的输出来分隔(子)测试用例?

这是一个我正在尝试的小玩具示例:

import pytest
import time
import asyncio

pytestmark = pytest.mark.asyncio
expected_duration = 1
accepted_error = 0.1

async def test_sleep():
    start = time.time()
    time.sleep(expected_duration)
    duration = time.time() - start
    assert abs(duration-expected_duration) < accepted_error

async def test_async_sleep():
    start = time.time()
    await asyncio.sleep(expected_duration)
    duration = time.time() - start
    assert abs(duration-expected_duration) < accepted_error

【问题讨论】:

  • 不确定它是否使用 asyncio,但你看过pytest-xdist 吗? pypi.org/project/pytest-xdist
  • 我并不真正关心 asyncio(curio 或 trio 就可以了),但我确实关心解决方案是异步的(非阻塞 IO)。为了提供更多的上下文,我有很多 Web 请求要测试(比如 50 到 100 个),我只想有一个解决方案可以一次测试所有这些请求(因此总测试持续时间接近最长测试的持续时间)。另一方面,我几乎没有 CPU 工作负载。这就是为什么我更喜欢异步解决方案而不是多线程解决方案。

标签: pytest python-asyncio python-trio pytest-asyncio curio


【解决方案1】:

不幸的是,pytest 在内部工作的方式,你不能真正在同一时间同时运行多个测试在同一调用 trio.run/asyncio.run/curio.run。 (这在某些方面也很好——它可以防止状态在测试之间泄漏,并且使用 trio 至少可以让您为不同的测试配置不同的 trio,例如将一个测试设置为使用 autojump clock 而另一个测试不使用。)

绝对最简单的选择是使用 pytest-xdist 在单独的线程中运行测试。您仍然可以在每个测试内部使用异步 - 所有这些异步库都支持在不同线程中运行不同的循环。

如果您真的需要使用异步并发,那么我认为您必须编写一个 pytest 测试函数,然后在该函数中执行您自己的调度和并发。如果你这样做,那么从 pytest 的角度来看,只有一个测试,所以要获得好的每次测试输出并不容易。我想你可以尝试使用pytest-subtests

【讨论】:

    【解决方案2】:

    现在有https://github.com/willemt/pytest-asyncio-cooperative

    今天2020-03-25,限制非常严重——你必须确保你的测试不共享anything(好吧,从技术上讲,不要共享可变状态)并且你不能使用mock.patch(从技术上讲不要模拟其他测试可能使用的任何东西)。

    您可以关注https://github.com/pytest-dev/pytest-asyncio/issues/69 的讨论,我认为这很难,但可以想出一种方法来标记每个夹具以允许或禁止并发使用,并安排测试以保留这些限制。

    【讨论】:

    • 我怎么会错过这条消息,这对于我的用例(主要是独立的 HTTP 请求)来说看起来很有希望。在这种特殊情况下,我什至不认为测试隔离有那么重要,因为在最坏的情况下,您只会得到假阴性。但也许我在最后一部分是错的。感谢您的更新。
    • 从 0.17.1 版本的 pytest-asyncio-cooperative 开始,围绕共享状态的限制已进一步放宽。例如,可以使用mock.patch,只要您使用“夹具锁”来防止竞争条件。
    【解决方案3】:

    按照 Nathaniel 的建议使用 pytest-subtests 似乎是一个可行的解决方案。这是使用 trio 解决的方法,它对名称以 io_ 开头的每个函数运行子测试。

    import pytest
    import sys
    import trio
    import inspect
    import re
    import time
    
    
    pytestmark = pytest.mark.trio
    io_test_pattern = re.compile("io_.*")
    
    
    async def tests(subtests):
    
        def find_io_tests(subtests, ignored_names):
            functions = inspect.getmembers(sys.modules[__name__], inspect.isfunction)
            for (f_name, function) in functions:
                if f_name in ignored_names:
                    continue
                if re.search(io_test_pattern, f_name):
                    yield (run, subtests, f_name, function)
    
        async def run(subtests, test_name, test_function):
            with subtests.test(msg=test_name):
                await test_function()
    
        self_name = inspect.currentframe().f_code.co_name
        async with trio.open_nursery() as nursery:
            for io_test in find_io_tests(subtests, {self_name}):
                nursery.start_soon(*io_test)
    
    
    accepted_error = 0.1
    
    async def io_test_1():
        await assert_sleep_duration_ok(1)
    
    async def io_test_2():
        await assert_sleep_duration_ok(2)
    
    async def io_test_3():
        await assert_sleep_duration_ok(3)
    
    async def io_test_4():
        await assert_sleep_duration_ok(4)
    
    async def assert_sleep_duration_ok(duration):
        start = time.time()
        await trio.sleep(duration)
        actual_duration = time.time() - start
        assert abs(actual_duration - duration) < accepted_error
    

    运行python -m pytest -v 输出:

    ============================ test session starts =============================
    platform darwin -- Python 3.7.0, pytest-4.6.2, py-1.8.0, pluggy-0.12.0
    plugins: asyncio-0.10.0, trio-0.5.2, subtests-0.2.1
    collected 1 item
    
    tests/stripe_test.py::tests PASSED                                     [100%]
    tests/stripe_test.py::tests PASSED                                     [100%]
    tests/stripe_test.py::tests PASSED                                     [100%]
    tests/stripe_test.py::tests PASSED                                     [100%]
    tests/stripe_test.py::tests PASSED                                     [100%]
    
    ========================== 1 passed in 4.07 seconds ==========================
    

    这并不完美,因为百分比仅与测试数量相关,而不与子测试数量相关(即io_* 此处标记的函数),但这似乎是一个好的开始。

    另请注意,使用了time.time(),因此它对 trio 和 asyncio 都有意义,但在实际用例中应使用 trio.current_time()

    使用 asyncio 可以实现相同的测试,您基本上必须替换三件事:

    • pytestmark = pytest.mark.triopytestmark = pytest.mark.asyncio
    • yield (run, subtests, f_name, function)yield run(subtests, f_name, function)
    • 最后,nursy 循环应该被替换为:
    await asyncio.gather(*find_io_tests(subtests, {self_name}))
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-12-02
      • 2023-03-26
      • 2013-04-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多