首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >pytest:按昂贵的操作分组测试

pytest:按昂贵的操作分组测试
EN

Stack Overflow用户
提问于 2021-04-10 16:10:24
回答 1查看 246关注 0票数 1

TL;DR:pytest:如何分组测试,使它们只在特定的配置下运行,而不运行其他的配置(与参数化相反,它会触发跨所有排列的测试的跨产品执行)。

我对pytest相当陌生,希望有办法做到这一点,但我没有看到。会很感激你的意见。我已经闲逛了一段时间了,还没有找到解决这种特殊情况的东西,我会继续寻找的。

我们的团队有一套现有的(阅读:继承)测试套件(900+ .py文件;质量是中等到低的,测试不是可重复的,也不是很好地自我清理,至少不是一致的),我们希望清理并转换到pytest (如果您认为其他框架可能更适合我们的情况,请建议它们)。通常,测试执行流程如下所示:

代码语言:javascript
复制
1. Create unit-under-test object (very expensive)
2. Apply default config          (also expensive; optional; each .py file opts-in/out)
3. Execute test function         (may tweak config, doesn't always clean up)
4. Tear down 2                   (partial cleanup, doesn't fully restore baseline state)
5. Repeat 2-4 for all test fns   (remember 2 is expensive!)

备注:

  • 无法缓存或保存/检索已创建的对象
  • 对象创建(步骤1)涉及属性的使用(名称/值对的集合)。有些是环境(命令行、env var等)适用于所有的测试。另一些则在对象创建时在.py文件中指定。因此,在.py文件中当然会有有限数量的属性配置文件(属性及其值的唯一排列)。
  • 当前,每个.py文件必须单独执行。
  • 步骤1和步骤2占运行所有测试所需时间的95%以上。
  • 额外的4%或更多是通过加载python模块(非常大的C++生成的APIs来与实际测试的.py代码进行交互)消耗的,这是目前每个.py文件都要支付的税款(回想一下它们分别运行)。
  • 因此,实际测试执行时间不到总时间的1%。

不用说,这里的主要目标是通过在使用相同属性配置文件(对象创建,步骤1)和默认配置选择/退出(步骤2)的文件之间分组测试,从而消除大部分这种开销。

大多数测试转换(我认为)都是简单的(例如,在固定设备中使用setup -> yield -> cleanup模式,等等)。

我们面临的挑战是找到一种方法来分组测试,使它们只为适用于它们的特定<property profile, default-config opt-in/out>置换而执行(记住属性配置文件是在运行时动态确定的),但每个组只执行一次昂贵的操作。参数化似乎与此相反,因为它允许对所有排列进行跨产品测试。此外,吡喃依赖插件似乎没有提供任何可能有帮助的东西(至少表面上没有)。而且,我还没有想出一种使用skipif()的方法,以避免在评估条件之前执行昂贵的操作(此外,考虑到这个场景中操作数的性质,我们如何编写条件?)

这需要我们自己编写插件吗?我最初几次尝试探究这个问题并不是很成功,不过我会坚持下去的。

我很感激你的见解。

谢谢你的关注(对不起,这有点长)。

EN

回答 1

Stack Overflow用户

发布于 2021-04-11 21:49:40

嗯,我找到了一个可行的解决方案,所以我想在这里分享一下。我不认为它是优雅的,但我可能会在这方面取得进展。

使用装饰器为给定的测试类指定属性和默认配置设置,可以将信息收集到类到概要文件的映射中。然后,pytest_runtestloop的替代实现允许按配置文件对测试进行分组。

我更愿意以某种方式使用固定装置,但在大多数尝试中,我最终都深入研究了pytest内部,需要导入_pytest并编写大量的代码。

下面是我想出的实现:

test.py:

代码语言:javascript
复制
from conftest import object_setup

@object_setup(
    properties = dict(PROP1=1, PROP2=2, PROP3=3),
    uses_default_config = True
)
class Test1:

    def test_1(self, the_object):
        print(f'-- T1: {the_object}')

@object_setup(
    properties = dict(PROP1=1, PROP2=2, PROP3=3),
    uses_default_config = True
)
class Test2:

    def test_2(self, the_object):
        print(f'-- T2: {the_object}')

@object_setup(uses_default_config = False)
class Test3:

    def test_3(self, the_object):
        print(f'-- T3: {the_object}')

def test_other():
    print(f'-- OTHER')

conftest.py:

代码语言:javascript
复制
import collections
import pytest

Profile = collections.namedtuple('Profile', 'properties uses_default_config')

# global variable -- ugh
the_obj = None

@pytest.fixture(scope='session')
def the_object(request):
    print(f'-> OBJ {the_obj}')
    yield the_obj
    print(f'<- OBJ {the_obj}')

class object_setup(object):
    by_class = {}

    def __init__(self, properties={}, uses_default_config=False):
        self.profile = Profile(
            properties=frozenset(tuple(prop) for prop in properties.items()),
            uses_default_config=uses_default_config
        )

    def __call__(self, cls):
        self.__class__.by_class[cls] = self.profile
        class Wrapped(cls):
            __obj_orig_cls__ = cls
            pass
        return Wrapped

def pytest_runtestloop(session):
    if session.testsfailed and not session.config.option.continue_on_collection_errors:
        raise session.Interrupted(
            "%d error%s during collection"
            % (session.testsfailed, "s" if session.testsfailed != 1 else "")
        )

    if session.config.option.collectonly:
        return True

    global the_obj
    by_class = object_setup.by_class
    grouped = { None: [] }

    for item in session.items:
        profile = None
        cls = getattr(item.cls, '__obj_orig_cls__', None)
        profile = by_class.get(cls, None)
        grouped.setdefault(profile, []).append(item)

    for profile, items in grouped.items():
        the_obj = { 'the': 'object', 'profile': profile }

        try:
            for i, item in enumerate(items):
                nextitem = items[i + 1] if i + 1 < len(items) else None
                item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
                if session.shouldfail:
                    raise session.Failed(session.shouldfail)
                if session.shouldstop:
                    raise session.Interrupted(session.shouldstop)
        finally:
            the_obj = None

    return True

以及产出:

代码语言:javascript
复制
pytest --setup-show test.py
============================== test session starts ===============================
============================== test session starts ===============================
platform darwin -- Python 3.9.2, pytest-6.2.3, py-1.10.0, pluggy-0.13.1
rootdir: /Users/userid/Documents/test_pytest/try
collected 4 items

test.py
        test.py::test_other.
SETUP    S the_object
        test.py::Test1::test_1 (fixtures used: request, the_object).
        test.py::Test2::test_2 (fixtures used: request, the_object).
TEARDOWN S the_object
SETUP    S the_object
        test.py::Test3::test_3 (fixtures used: request, the_object).
TEARDOWN S the_object

=============================== 4 passed in 0.01s ================================

更详细的输出显示,在每种情况下都使用正确的配置文件,并且对象创建/销毁发生在正确的点上。

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/67036524

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档