使用unittest.mock对装饰器进行Autospec

2 投票
3 回答
718 浏览
提问于 2025-04-18 08:36

假设我有一个简单的装饰器方法,像下面这样:

def my_decorator(fn):
  def _wrapper(*args, **kwargs):
    print 'Calling decorated function'
    fn(*args, **kwargs)
  return _wrapper

class Foo(object):
  @my_decorator
  def incr(self, x):
    return x+1

这个装饰器会“抹去”方法的签名,以便于自动规范检查:

>>> mock_foo = mock.create_autospec(Foo, instance=True)
>>> mock_foo.incr(1, 2, 3, 4)
<MagicMock name='mock.incr()' id='23032592'>

这应该会引发错误:

TypeError: <lambda>() takes exactly 2 arguments (5 given)

我曾经因为关键字参数的拼写错误而出现过这样的bug。

有没有办法编写这个装饰器(或者给自动规范检查一个“提示”),让这些错误能够被捕捉到呢?

3 个回答

0

一个同事给我推荐了这个装饰器库

from decorator import decorator

@decorator
def my_decorator(fn, *args, **kwargs):
  print 'Calling decorated function'
  return fn(*args, **kwargs)

class Foo(object):
  @my_decorator
  def incr(self, x):
    return x+1

@decorator这个东西能做到一些很厉害的事情,它可以让你在使用@my_decorator的时候,不会让被装饰的函数的签名被隐藏。

1

你可以使用functools.wraps,mock.create_autospec也能理解它:

from functools import wraps
from unittest import mock


def my_decorator(fn):
   @wraps(fn)
   def _wrapper(*args, **kwargs):
       print('Calling decorated function')
       fn(*args, **kwargs)
   return _wrapper


class Foo(object):
    @my_decorator
    def incr(self, x):
        return x + 1


if __name__ == '__main__':
    mock_foo = mock.create_autospec(Foo, instance=True)
    mock_foo.incr(1, 2, 3, 4)

如果你把上面的代码放到一个文件里运行,你会看到最后一行的错误信息:

TypeError: 参数太多了

如果没有使用@wraps,这个脚本会正常结束,返回代码是0。

1

我觉得autospec不能直接做到这一点。不过,你可以在装饰器里做一些小技巧,让你能测试未被装饰的函数。如果你让你的装饰器保存一个未装饰函数的引用:

def my_decorator(fn):
  def _wrapper(*args, **kwargs):
    print 'Calling decorated function'
    fn(*args, **kwargs)
  _wrapper._orig = fn
  return _wrapper

你可以通过被模拟的装饰函数来访问它:

>>> mock_incr = mock.create_autospec(Foo.incr)
>>> mock_incr(1,3,4,5,5)               # Decorated function doesn't fail.
<MagicMock name='mock()' id='8734864'>
>>> mock_incr._orig(1,3,4,5,5)         # But the original does, which is what we want
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib64/python2.6/site-packages/mock.py", line 954, in __call__
    _mock_self._mock_check_sig(*args, **kwargs)
TypeError: <lambda>() takes exactly 3 arguments (6 given)
>>> mock_incr._orig(1,3)
<MagicMock name='mock._orig()' id='8739664'>

不过,如果你对整个实例使用autospec,这样就不行了。我也不太清楚为什么。

>>> mock_foo = mock.create_autospec(Foo, instance=True)
>>> mock_foo.incr(1,3,4,5)             # We expect this to not raise an exception
<MagicMock name='mock.incr2()' id='8758416'>
>>> mock_foo.incr._orig(1,3,4,5)       # But we were hoping this would :(
<MagicMock name='mock.incr._orig()' id='8740624'>

另外,值得一提的是Venusian,它可以改变装饰器与被装饰方法绑定的方式,专门为了解决这个问题。不过,这可能比你想要的要复杂一些。

撰写回答