在函数装饰器中使用pytest灯具

时间:2018-12-18 20:43:13

标签: python-2.7 pytest

我想为我的测试功能构建一个装饰器,它有多种用途。其中之一是帮助向生成的junitxml添加属性。

我知道有一个名为record_property的{​​{3}}内置pytest可以做到这一点。如何在装饰器中使用此灯具?

def my_decorator(arg1):
    def test_decorator(func):
        def func_wrapper():
            # hopefully somehow use record_property with arg1 here
            # do some other logic here
            return func()
        return func_wrapper
    return test_decorator

@my_decorator('some_argument')
def test_this():
    pass # do actual assertions etc.

我知道我可以将灯具直接传递到每个测试功能中,并在测试中使用它,但是我有很多测试,这样做似乎非常多余。

此外,我知道我可以使用conftest.py并创建一个自定义标记并在装饰器中调用它,但是我有很多conftest.py文件,因此我不能单独管理所有文件我无法执行。

最后,尝试将灯具直接导入到我的装饰器模块中,然后使用它会导致错误-因此也是不可行的。

感谢您的帮助

1 个答案:

答案 0 :(得分:0)

有点晚了,但我在我们的代码库中遇到了同样的问题。我可以找到解决方案,但它相当笨拙,因此我不保证它适用于旧版本或将来会流行。

因此我问是否有更好的解决方案。您可以在这里查看:How to use pytest fixtures in a decorator without having it as argument on the decorated function

这个想法基本上是注册被装饰的测试函数,然后欺骗 pytest 认为他们需要在他们的参数列表中的夹具:

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

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",)