我试图在装饰器中使用一个夹具来装饰测试功能。目的是为测试提供注册的测试数据。有两种选择:
手动导入是微不足道的。我只需要全局注册测试数据,然后可以根据其名称在测试中访问它,自动导入比较棘手,因为它应该使用 pytest 夹具。
最后会怎样:
@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
Run Code Online (Sandbox Code Playgroud)
我做了什么:
装饰器在类变量中全局注册测试数据。它还使用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
Run Code Online (Sandbox Code Playgroud)
这会导致在第一个测试用例中出现错误,正如装饰器所期望的那样my_import_fixture,但不幸的是它不是由 pytest 传递的,因为 pytest 只是查看未装饰函数的签名。
此时它变得很棘手,因为我们必须告诉 pytestmy_import_fixture作为参数传递,即使原始测试函数的签名不包含它。我们通过添加夹具名称来覆盖pytest_collection_modifyitems钩子并操作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",)
Run Code Online (Sandbox Code Playgroud)
为了完整性,导入夹具的位代码:
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
Run Code Online (Sandbox Code Playgroud)
现在我的问题是是否有更好(更多 pytest 本机)的方法来做到这一点。之前有一个类似的问题 ,但从未得到回答,我会将我的问题链接到它,因为它本质上描述了如何解决它的hacky方法(至少在pytest 6.1.2和python 3.7.1行为下)。
有些人可能会争辩说,我可以移除夹具并MyImporter在装饰器中创建一个对象。然后我将面临与request夹具相同的问题,但可以通过将夹具func.__name__而不是request夹具传递给构造函数来简单地避免这种情况。
不幸的是,由于我在my_import_fixture. 当然我可以复制它,但它变得非常复杂,因为我使用其他灯具,它们也有一些配置和清理逻辑等等。最后,这将是需要保持同步的重复代码。
我也不想my_import_fixture成为,autouse因为这意味着对测试的一些要求。
我希望这个答案对一年多后的人有帮助。根本问题是当你这样做时
@functools.wraps(func)
def wrapper(*args: Any, my_import_fixture, **kwargs: Any):
. . .
Run Code Online (Sandbox Code Playgroud)
的签名wrapper就是 的签名func。 my_import_fixture不是签名的一部分。一旦我明白这就是问题所在,我就在这里得到了一个关于如何快速修复它的非常有用的答案How can Iwrapp a python function in a way that works withspectr.signature?
要让 pytest 传递my_import_fixture给你的包装器,请执行以下操作:
@functools.wraps(func)
def wrapper(*args: Any, my_import_fixture, **kwargs: Any):
. . .
Run Code Online (Sandbox Code Playgroud)
PEP-362解释了其工作原理。感谢@Andrej Kesely 回答了相关问题。
抱歉 - 我稍微简化了代码,因为我解决的问题与您的问题略有不同(我需要包装器来访问固定装置,request即使包装的测试用例没有使用它)。不过,相同的解决方案应该适合您。
| 归档时间: |
|
| 查看次数: |
259 次 |
| 最近记录: |