TL;DR:pytest:如何分组测试,使它们只在特定的配置下运行,而不运行其他的配置(与参数化相反,它会触发跨所有排列的测试的跨产品执行)。
我对pytest相当陌生,希望有办法做到这一点,但我没有看到。会很感激你的意见。我已经闲逛了一段时间了,还没有找到解决这种特殊情况的东西,我会继续寻找的。
我们的团队有一套现有的(阅读:继承)测试套件(900+ .py文件;质量是中等到低的,测试不是可重复的,也不是很好地自我清理,至少不是一致的),我们希望清理并转换到pytest (如果您认为其他框架可能更适合我们的情况,请建议它们)。通常,测试执行流程如下所示:
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)和默认配置选择/退出(步骤2)的文件之间分组测试,从而消除大部分这种开销。
大多数测试转换(我认为)都是简单的(例如,在固定设备中使用setup -> yield -> cleanup模式,等等)。
我们面临的挑战是找到一种方法来分组测试,使它们只为适用于它们的特定<property profile, default-config opt-in/out>置换而执行(记住属性配置文件是在运行时动态确定的),但每个组只执行一次昂贵的操作。参数化似乎与此相反,因为它允许对所有排列进行跨产品测试。此外,吡喃依赖插件似乎没有提供任何可能有帮助的东西(至少表面上没有)。而且,我还没有想出一种使用skipif()的方法,以避免在评估条件之前执行昂贵的操作(此外,考虑到这个场景中操作数的性质,我们如何编写条件?)
这需要我们自己编写插件吗?我最初几次尝试探究这个问题并不是很成功,不过我会坚持下去的。
我很感激你的见解。
谢谢你的关注(对不起,这有点长)。
发布于 2021-04-11 21:49:40
嗯,我找到了一个可行的解决方案,所以我想在这里分享一下。我不认为它是优雅的,但我可能会在这方面取得进展。
使用装饰器为给定的测试类指定属性和默认配置设置,可以将信息收集到类到概要文件的映射中。然后,pytest_runtestloop的替代实现允许按配置文件对测试进行分组。
我更愿意以某种方式使用固定装置,但在大多数尝试中,我最终都深入研究了pytest内部,需要导入_pytest并编写大量的代码。
下面是我想出的实现:
test.py:
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:
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以及产出:
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 ================================更详细的输出显示,在每种情况下都使用正确的配置文件,并且对象创建/销毁发生在正确的点上。
https://stackoverflow.com/questions/67036524
复制相似问题