【问题标题】:Writing decorator for pytest test method为 pytest 测试方法编写装饰器
【发布时间】:2013-05-28 14:14:57
【问题描述】:

假设如下结构:

class SetupTestParam(object):
    def setup_method(self, method):
        self.foo = bar()

    @pytest.fixture
    def some_fixture():
        self.baz = 'foobar'

我使用SetupTestParam 作为测试类的父类。

class TestSomething(SetupTestParam):
    def test_a_lot(self, some_fixture):
        with self.baz as magic:
            with magic.fooz as more_magic:
                 blah = more_magic.much_more_magic() # repetative bleh
            ... # not repetative code here
            assert spam == 'something cool'

现在,编写测试变得重复(使用语句),我想编写一个装饰器来减少代码行数。但是pytest和函数签名有问题。

我发现 library 应该会有所帮助,但我无法让它发挥作用。

我在SetupTestParam 班级中创建了一个classmethod

@classmethod
@decorator.decorator
def this_is_decorator(cls, f):
    def wrapper(self, *args, **kw):
        with self.baz as magic:
            with magic.fooz as more_magic:
                 blah = more_magic.much_more_magic() # repetative bleh
            return f(self, *args)
    return wrapper

装饰test_a_lot方法后,收到错误TypeError: transaction_decorator() takes exactly 1 argument (2 given)

谁能解释一下我做错了什么? (我假设测试方法中的self 有问题?)

【问题讨论】:

    标签: python decorator pytest


    【解决方案1】:

    链接装饰器并不是最简单的事情。一种解决方案可能是将两个装饰器分开。保留classmethod 但将decorator.decorator 移到末尾:

    @classmethod
    def this_is_decorator(cls, f):
        def wrapper(self, *args, **kw):
            with self.baz as magic:
                with magic.fooz as more_magic:
                     blah = more_magic.much_more_magic() # repetative bleh
                return f(self, *args)
        return decorator.decorator(wrapper, f)
    

    也许这对你有用。

    【讨论】:

    • 我如何从包装函数中的 SetupTestParam 访问 self.foo?
    • 是的。我也想办法休息了。以下是 pytest 中显示的包装器参数:self = <function test_a_lot at 0x24820c8> args = (<TestSomething object at 0x29c77d0>, None, None, None, None), kw = {} 所以现在不是self.baz 而是args[0].baz。我只需要返回f(*args)
    • 太棒了。也许您可以回答自己的问题,将答案中的所有部分和 cmets 放在一起。鼓励自己回答问题:meta.stackexchange.com/questions/2706/…
    • 是的,我会在完成装饰器调整后立即执行此操作。使用args[0] 看起来很糟糕。有什么方法可以将函数的实例(在这种情况下为 self 参数)传递给 wrapper ?我希望看到wrapperself 成为test_a_lotself 的参考。
    【解决方案2】:

    经过一些调整并意识到我需要将参数传递给装饰器后,我选择将其编写为一个类:

    class ThisIsDecorator(object):
        def __init__(self, param):
            self.param = param   # Parameter may vary with the function being decorated
        def __call__(self, fn):
            wraps(fn) # [1]
            def wrapper(fn, fn_self, *args): # [2] fn_self refers to original self param from function fn (test_a_lot) [2]
                with fn_self.baz as fn_self.magic: # I pass magic to fn_self to make magic accesible in function fn (test_a_lot)
                    with fn_self.magic.fooz as more_magic:
                        blah = self.param.much_more_magic() # repetative bleh
                return fn(fn_self, *args)
            return decorator.decorator(wrapper, fn) 
    

    [1] 我使用wraps 拥有原始fn __name____module____doc__

    [2] 传递给wrapper 的参数是self = <function test_a_lot at 0x24820c8> args = (<TestSomething object at 0x29c77d0>, None, None, None, None), kw = {},所以我将args[0] 取出为fn_self

    原版(不传参数):

     @classmethod
     def this_is_decorator(cls, fn):
         @wraps(fn)
         def wrapper(fn, fn_self, *args):
             with fn_self.baz as fn_self.magic:
                 with fn_self.magic.fooz as more_magic:
                     blah = more_magic.much_more_magic() # repetative bleh
                 return fn(fn_self, *args)
         return decorator.decorator(wrapper,fn)
    

    感谢 Mike Muller 指出正确的方向。

    【讨论】:

    • 如果迈克的回答有帮助,通常很高兴支持它... :)
    【解决方案3】:

    这是定义此方法时按时间顺序发生的情况。

    1. this_is_decorator 已创建(未调用)。
    2. decorator.decorator(this_is_decorator) 被调用。这将返回一个新函数,该函数变为 this_is_decorator 并具有相同的用法。
    3. classmethod(this_is_decorator) 被调用,其结果是一个接受(cls, f) 并返回wrapper 的类方法。
    4. 稍后在运行时,对this_is_decorator 的调用将返回wrapper

    但是考虑到this_is_decorator 是一个类方法,我不清楚这就是你想要的。我猜你可能想要更多这样的东西:

    from decorator import decorator
    @decorator
    def mydecorator(f):
      def wrapper(cls, *args, **kw):
        # ... logging, reporting, caching, whatever
        return f(*args, **kw)
      return wrapper
    
    class MyClass(object):
      @classmethod
      @mydecorator
      def myclsmethod(a, b, c):
        # no cls or self value accepted here; this is a function not a method
        # ...
    

    这里你的装饰器是在你的类之外定义的,因为它把一个普通的函数变成了一个classmethod(因为你可能想在其他地方使用它)。这里的执行顺序是:

    1. mydecorator 已定义,未调用。
    2. decorator(mydecorator) 被调用,结果变成 mydecorator
    3. 开始创建MyClass
    4. myclsmethod 已创建。它是一个普通的函数,而不是一个方法。 VM 内部存在差异,因此您不必为方法显式提供 clsself 参数。
    5. myclsmethod 被传递给 mydecorator(它本身已经被装饰过)并且结果 (wrapper) 仍然是一个函数而不是一个方法
    6. mydecorator 的结果被传递给classmethod,它返回绑定到MyClass.myclsmethod 的实际类方法。
    7. MyClass 的定义完成。
    8. 稍后当MyClass.myclsmethod(a, b, c) 被调用时,wrapper 执行,然后调用原始的myclsmethod(a, b, c) 函数(它称为f)而不提供cls 参数。

    由于您还需要准确地保留参数列表,因此即使参数的名称也保留在修饰函数中,除了额外的初始参数cls,那么您可以通过这种方式实现mydecorator

    from decorator import decorator
    from inspect import getargspec
    
    @decorator
    def mydecorator(func):
      result = [None]  # necessary so exec can "return" objects
      namespace = {'f': func, 'result': result}
      source = []
      add = lambda indent, line: source.append(' ' * indent + line)  # shorthand
      arglist = ', '.join(getargspec(func).args)  # this does not cover keyword or default args
      add(0, 'def wrapper(cls, %s):' % (arglist,))
      add(2, 'return f(%s)' % (arglist,))
      add(0, 'result[0] = wrapper')  # this is how to "return" something from exec
      exec '\n'.join(source) in namespace
      return result[0]  # this is wrapper
    

    这有点难看,但这是我知道的根据数据动态设置函数参数列表的唯一方法。如果返回 lambda 没问题,您可以使用 eval 代替 exec,这样就无需写入数组,但其他方面基本相同。

    【讨论】:

    • 我使用classmethod 来装饰this_is_decorator,因为我想在继承的class TestSomething 中使用这个装饰器。我之所以使用decorator.decorator 是因为 pytest 夹具,我需要装饰器调用具有正确签名的函数。基本上,pytest 必须看到 f(some_fixture) 而不是 f(*args) 因为 *args 不是任何固定装置。
    • 让我看看我是否理解正确。您希望一个或多个装饰器协作 (1) 生成一个类方法 (2) 装饰一个不接受 cls 参数的函数 (3) 装饰一个可以分析其参数列表的函数,如 inspect.getargspec 和不会只接受可变参数。
    • 如果一切正确,我认为最好的办法是使用getargspec 获取参数列表,将wrapper 的源构建为字符串(带有额外的初始cls 参数),使用exec 语句创建wrapper 函数,并返回它。所有这些都将在您的装饰器中完成。
    • 也许你是对的,但我无法想象,你能根据我提供的例子给我看一个简单的例子吗?或者只是将其编辑为您的答案。
    猜你喜欢
    • 2019-08-30
    • 2014-09-20
    • 1970-01-01
    • 2015-02-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-26
    • 1970-01-01
    相关资源
    最近更新 更多