pytest:测试标记 class 覆盖测试功能上的相同标记

pytest: Mark on test class overrides same mark on test function

我正在使用 pytest.mark 给我的测试 kwargs。但是,如果我在 class 和 class 中的测试中使用相同的标记,则当对两者使用相同的 kwargs 时,class 的标记会覆盖函数上的标记.

import pytest

animal = pytest.mark.animal


@animal(species='croc')  # Mark the class with a kwarg
class TestClass(object):

    @animal(species='hippo')  # Mark the function with new kwarg
    def test_function(self):
        pass


@pytest.fixture(autouse=True)  # Use a fixture to inspect my function
def animal_inspector(request):
    print request.function.animal.kwargs  # Show how the function object got marked


# prints {'species': 'croc'} but the function was marked with 'hippo'

我的河马去哪儿了,我怎样才能把他找回来?

不幸的是 pytest bugs related to this,我猜您 运行 属于其中之一。我发现的那些与子类化有关,但你没有在那里做。

所以我一直在 pytest 代码中挖掘并弄清楚为什么会这样。函数上的标记在导入时应用于函数,但 class 和模块级别标记在测试收集之前不会应用于函数级别。函数标记首先发生并将它们的 kwargs 添加到函数中。然后 class 标记覆盖任何相同的 kwargs,模块标记进一步覆盖任何匹配的 kwargs。

我的解决方案是简单地创建我自己的修改后的 MarkDecorator,它在将 kwargs 添加到标记之前对其进行过滤。基本上,首先设置的 kwarg 值(似乎总是由函数装饰器设置)将始终是标记上的值。理想情况下,我认为应该在 MarkInfo class 中添加此功能,但由于我的代码没有创建它的实例,所以我选择了 创建的实例:MarkDecorator。请注意,我只更改了源代码中的两行(关于 keys_to_add 的位)。

from _pytest.mark import istestfunc, MarkInfo
import inspect


class TestMarker(object):  # Modified MarkDecorator class
    def __init__(self, name, args=None, kwargs=None):
        self.name = name
        self.args = args or ()
        self.kwargs = kwargs or {}

    @property
    def markname(self):
        return self.name # for backward-compat (2.4.1 had this attr)

    def __repr__(self):
        d = self.__dict__.copy()
        name = d.pop('name')
        return "<MarkDecorator %r %r>" % (name, d)

    def __call__(self, *args, **kwargs):
        """ if passed a single callable argument: decorate it with mark info.
            otherwise add *args/**kwargs in-place to mark information. """
        if args and not kwargs:
            func = args[0]
            is_class = inspect.isclass(func)
            if len(args) == 1 and (istestfunc(func) or is_class):
                if is_class:
                    if hasattr(func, 'pytestmark'):
                        mark_list = func.pytestmark
                        if not isinstance(mark_list, list):
                            mark_list = [mark_list]
                        mark_list = mark_list + [self]
                        func.pytestmark = mark_list
                    else:
                        func.pytestmark = [self]
                else:
                    holder = getattr(func, self.name, None)
                    if holder is None:
                        holder = MarkInfo(
                            self.name, self.args, self.kwargs
                        )
                        setattr(func, self.name, holder)
                    else:
                        # Don't set kwargs that already exist on the mark
                        keys_to_add = {key: value for key, value in self.kwargs.items() if key not in holder.kwargs}
                        holder.add(self.args, keys_to_add)
                return func
        kw = self.kwargs.copy()
        kw.update(kwargs)
        args = self.args + args
        return self.__class__(self.name, args=args, kwargs=kw)


# Create my Mark instance. Note my modified mark class must be imported to be used
animal = TestMarker(name='animal')

# Apply it to class and function
@animal(species='croc')  # Mark the class with a kwarg
class TestClass(object):

    @animal(species='hippo')  # Mark the function with new kwarg
    def test_function(self):
        pass

# Now prints {'species': 'hippo'}  Yay!