Use pytest fixture in a function decorator

守給你的承諾、 提交于 2021-02-08 04:41:25

问题


I want to build a decorator for my test functions which has several uses. One of them is helping to add properties to the generated junitxml.

I know there's a fixture built-in pytest for this called record_property that does exactly that. How can I use this fixture inside my decorator?

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.

I know I can pass the fixture directly into every test function and use it in the tests, but I have a lot of tests and it seems extremely redundant to do this.

Also, I know I can use conftest.py and create a custom marker and call it in the decorator, but I have a lot of conftest.py files and I don't manage all of them alone so I can't enforce it.

Lastly, trying to import the fixture directly in to my decorator module and then using it results in an error - so that's a no go also.

Thanks for the help


回答1:


It's a bit late but I came across the same problem in our code base. I could find a solution to it but it is rather hacky, so I wouldn't give a guarantee that it works with older versions or will prevail in the future.

Hence I asked if there is a better solution. You can check it out here: How to use pytest fixtures in a decorator without having it as argument on the decorated function

The idea is to basically register the test functions which are decorated and then trick pytest into thinking they would require the fixture in their argument list:

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


来源:https://stackoverflow.com/questions/53840765/use-pytest-fixture-in-a-function-decorator

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!