我试图在装饰器中使用一个fixture,该装饰器旨在装饰测试函数。目的是为测试提供注册的测试数据。有两个选项:
- 进口自动
- 手动导入
手工导入是微不足道的。我只需要全局注册测试数据,然后可以根据其名称在测试中访问它,自动导入更棘手,因为它应该使用pytest fixture。
最后会是什么样子呢:
@RegisterTestData("some identifier")
def test_automatic_import(): # everything works automatic, so no import fixture is needed
# Verify that the test data was correctly imported in the test system
pass
@RegisterTestData("some identifier", direct_import=False)
def test_manual_import(my_import_fixture):
my_import_fixture.import_all()
# Verify that the test data was correctly imported in the test system
我做了什么:
装饰器将testdata全局注册到一个类变量中。它还使用usefixtures
标记用相应的夹具标记测试,以防它不使用它。这是必要的,否则pytest将不会为测试创建my_import_fixture
对象:
class RegisterTestData:
# global testdata registry
testdata_identifier_map = {} # Dict[str, List[str]]
def __init__(self, testdata_identifier, direct_import = True):
self.testdata_identifier = testdata_identifier
self.direct_import = direct_import
self._always_pass_my_import_fixture = False
def __call__(self, func):
if func.__name__ in RegisterTestData.testdata_identifier_map:
RegisterTestData.testdata_identifier_map[func.__name__].append(self.testdata_identifier)
else:
RegisterTestData.testdata_identifier_map[func.__name__] = [self.testdata_identifier]
# We need to know if we decorate the original function, or if it was already
# decorated with another RegisterTestData decorator. This is necessary to
# determine if the direct_import fixture needs to be passed down or not
if getattr(func, "_decorated_with_register_testdata", False):
self._always_pass_my_import_fixture = True
setattr(func, "_decorated_with_register_testdata", True)
@functools.wraps(func)
@pytest.mark.usefixtures("my_import_fixture") # register the fixture to the test in case it doesn't have it as argument
def wrapper(*args: Any, my_import_fixture, **kwargs: Any):
# Because of the signature of the wrapper, my_import_fixture is not part
# of the kwargs which is passed to the decorated function. In case the
# decorated function has my_import_fixture in the signature we need to pack
# it back into the **kwargs. This is always and especially true for the
# wrapper itself even if the decorated function does not have
# my_import_fixture in its signature
if self._always_pass_my_import_fixture or any(
"hana_import" in p.name for p in signature(func).parameters.values()
):
kwargs["hana_import"] = hana_import
if self.direct_import:
my_import_fixture.import_all()
return func(*args, **kwargs)
return wrapper
这会在第一个测试用例中导致错误,因为decorator期望通过my_import_fixture
,但不幸的是它不是pytest,因为pytest只查看未修饰函数的签名。
my_import_fixture
作为参数,即使原始测试函数的签名不包含它。我们覆盖pytest_collection_modifyitems
钩子,并通过添加fixture名称来操作相关测试函数的argnames
:
def pytest_collection_modifyitems(config: Config, items: List[Item]) -> None:
for item in items:
if item.name in RegisterTestData.testdata_identifier_map and "my_import_fixture" not in item._fixtureinfo.argnames:
# Hack to trick pytest into thinking the my_import_fixture is part of the argument list of the original function
# Only works because of @pytest.mark.usefixtures("my_import_fixture") in the decorator
item._fixtureinfo.argnames = item._fixtureinfo.argnames + ("my_import_fixture",)
为完整起见,输入设备的位码:
class MyImporter:
def __init__(self, request):
self._test_name = request.function.__name__
self._testdata_identifiers = (
RegisterTestData.testdata_identifier_map[self._test_name]
if self._test_name in RegisterTestData.testdata_identifier_map
else []
)
def import_all(self):
for testdata_identifier in self._testdata_identifiers:
self.import_data(testdata_identifier)
def import_data(self, testdata_identifier):
if testdata_identifier not in self._testdata_identifiers: #if someone wants to manually import single testdata
raise Exception(f"{import_metadata.identifier} is not registered. Please register it with the @RegisterTestData decorator on {self._test_name}")
# Do the actual import logic here
@pytest.fixture
def my_import_fixture(request /*some other fixtures*/):
# Do some configuration with help of the other fixtures
importer = MyImporter(request)
try:
yield importer
finally:
# Do some cleanup logic
现在我的问题是,是否有更好(更pytest native)的方法来做到这一点。以前有一个类似的问题,但是从来没有得到回答,我将把我的问题链接到它,因为它本质上描述了一种如何解决它的hack方法(至少使用pytest 6.1.2和python 3.7.1的行为)。
有些人可能会争辩说,我可以删除fixture并在decorator中创建MyImporter
对象。然后,对于request
fixture,我将面临同样的问题,但可以简单地通过将func.__name__
而不是request
fixture传递给构造函数来避免这个问题。
不幸的是,由于我在my_import_fixture
中的配置和清理逻辑,这失败了。当然,我可以复制它,但它会变得超级复杂,因为我使用了其他fixture,这些fixture也有一些配置和清理逻辑等等。最后,这将是重复的代码,需要保持同步。
我也不希望my_import_fixture
是autouse
,因为它暗示了测试的一些要求。
我希望这个答案对一年以后的人有帮助。潜在的问题是,当您执行
时 @functools.wraps(func)
def wrapper(*args: Any, my_import_fixture, **kwargs: Any):
. . .
wrapper
的签名就是func
的签名。my_import_fixture
不是签名的一部分。一旦我明白了这是问题所在,我就得到了一个非常有用的答案,关于如何在这里快速修复它。我如何以与inspect.signature一起工作的方式包装python函数?
要让pytest将my_import_fixture
传递给包装器,执行如下操作:
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any):
# Pytest will pass `my_import_fixture` in kwargs.
# If the wrapped func needs `my_import_fixture` then we need to keep it in kwargs.
# If func doesn't expect a `my_import_fixture` argument then we need to remove it from kwargs.
if 'my_import_fixture' in inspect.signature(func).parameters.keys():
my_import_fixture = kwargs['my_import_fixture']
else:
my_import_fixture = kwargs.pop('my_import_fixture')
# Do whatever it is you need to do with `my_import_fixture` here.
# I'm omitting that specific logic from this answer
# . . .
# Now call the wrapped func with the correct arguments
return func(*args, **kwargs)
# If the wrapped func already uses the `my_import_fixture` fixture
# then we don't need to do anything. `my_import_fixture` will already be
# part of the wrapper's signature.
# If wrapped doesn't use `my_import_fixture` we need to add it to the
# signature of the wrapper in a way that pytest will notice.
if 'my_import_fixture' not in inspect.signature(func).parameters.keys():
original_signature = inspect.signature(func)
wrapper.__signature__ = original_signature.replace(
parameters=(
list(original_signature.parameters.values()) +
[inspect.Parameter('my_import_fixture', inspect.Parameter.POSITIONAL_OR_KEYWORD)]
)
)
return wrapper
PEP-362解释了这是如何工作的。感谢@Andrej Kesely,他回答了相关问题。
抱歉-我已经简化了代码一点,因为我解决的问题与您的问题略有不同(我需要包装器访问request
fixture,即使包装的测试用例没有使用它)。同样的解决方案应该适用于您。