pytest 发现遗漏了装饰方法

pytest discovery misses decorated methods

我希望能够以类似于这样的格式编写一堆测试:

class TestPytest:
    @given(3)
    @expect(3)
    def test_passes(self, g, e):
        assert g == e

    @given(3)
    @expect(4)
    def test_fails(self, g, e):
        assert g == e

    def test_boring(self): # for comparison
        pass

(我不认为这是个好主意,但我会进一步说明它,所以它并不像看起来那么奇怪。)

为此,我尝试编写了这些装饰器:

import functools

class WrappedTest(object):
    def __init__(self, f):
        self.func = f
        self.given = []
        self.expects = []

    def __get__(self, instance, owner):
        @functools.wraps(self.func)
        def call(*a, **kw):
            return self.func(instance, self.given, self.expects,
                             *a, **kw)
        return call

def given(*objects):
    def wrapped(test):
        if not isinstance(test, WrappedTest):
            test_tmp = WrappedTest(test)
            test = functools.update_wrapper(test_tmp, test)

        test.given.extend(objects)
        return test
    return wrapped

def expect(*objects):
    def wrapped(test):
        if not isinstance(test, WrappedTest):
            test_tmp = WrappedTest(test)
            test = functools.update_wrapper(test_tmp, test)

        test.expects.extend(objects)
        return test
    return wrapped

但是当我尝试 运行 这个测试时,pytest 没有找到 test_passestest_fails。它确实找到 test_boring.

我的工作假设是我没有正确包装测试方法。它们显示为函数而不是方法:

>>> test_pytest.TestPytest().test_fails
<function test_pytest.test_fails>

>>> test_pytest.TestPytest().test_boring
<bound method TestPytest.test_boring of <test_pytest.TestPytest instance at 0x101f3dab8>>

但我不确定如何解决这个问题。我试过将 functools.wraps(self.func) 更改为 functools.wraps(self.func.__get__(instance, owner)),理论上它会用绑定方法而不是函数包装。但那只是一种猜测,并没有奏效。

我知道 pytest 能够找到正确编写的修饰函数,所以大概我做错了什么,但我不确定是什么。

看来我关于包装的做法是错误的。查看 pytest 源代码,它对待嵌套 类 的方式与方法不同。它通过忽略 __get____dict__ 访问成员,因此 WrappedTest 没有成功伪装成一个方法。

我已经用函数替换了 WrappedTest 实例,它似乎工作正常(即使没有 @functools.wraps 行):

import functools
from collections import namedtuple

def wrap_test_method(meth):
    if hasattr(meth, '_storage'):
        return meth

    Storage = namedtuple('Storage', ['given', 'expects'])
    sto = Storage(given=[], expects=[])

    @functools.wraps(meth)
    def new_meth(self, *a, **kw):
        return meth(self, sto.given, sto.expects, *a, **kw)
    new_meth._storage = sto

    return new_meth

def given(*objects):
    def decorator(test_method):
        new_test_method = wrap_test_method(test_method)
        new_test_method._storage.given.extend(objects)
        return new_test_method

    return decorator

def expect(*objects):
    def decorator(test_method):
        new_test_method = wrap_test_method(test_method)
        new_test_method._storage.expects.extend(objects)
        return new_test_method

    return decorator