修补函数的__call__

时间:2015-12-14 06:41:00

标签: python time mocking python-mock

我需要在测试中修补当前日期时间。我正在使用这个解决方案:

def _utcnow():
    return datetime.datetime.utcnow()


def utcnow():
    """A proxy which can be patched in tests.
    """
    # another level of indirection, because some modules import utcnow
    return _utcnow()

然后在我的测试中,我做了类似的事情:

    with mock.patch('***.utils._utcnow', return_value=***):
        ...

但是今天我想到了一个想法,即通过修补函数__call__的{​​{1}}而不是添加utcnow,我可以简化实现。

这对我不起作用:

_utcnow

如何优雅地做到这一点?

3 个答案:

答案 0 :(得分:13)

<强> [编辑]

也许这个问题最有趣的部分是为什么我无法修补somefunction.__call__

由于该功能不使用__call__的代码,但__call__(方法包装器对象)使用函数代码。

我没有找到任何有关此问题的资料来源,但我可以证明这一点(Python2.7):

>>> def f():
...     return "f"
... 
>>> def g():
...     return "g"
... 
>>> f
<function f at 0x7f1576381848>
>>> f.__call__
<method-wrapper '__call__' of function object at 0x7f1576381848>
>>> g
<function g at 0x7f15763817d0>
>>> g.__call__
<method-wrapper '__call__' of function object at 0x7f15763817d0>

f代码替换g代码:

>>> f.func_code = g.func_code
>>> f()
'g'
>>> f.__call__()
'g'

当然ff.__call__引用不会更改:

>>> f
<function f at 0x7f1576381848>
>>> f.__call__
<method-wrapper '__call__' of function object at 0x7f1576381848>

恢复原始实现并复制__call__引用:

>>> def f():
...     return "f"
... 
>>> f()
'f'
>>> f.__call__ = g.__call__
>>> f()
'f'
>>> f.__call__()
'g'

这对f功能没有任何影响。 注意:在Python 3中,您应该使用__code__而不是func_code

我希望有人能指出我解释这种行为的文档。

你有办法解决这个问题:在utils你可以定义

class Utcnow(object):
    def __call__(self):
        return datetime.datetime.utcnow()


utcnow = Utcnow()

现在你的补丁可以像魅力一样工作。

遵循我认为是实施测试的最佳方式的原始答案。

我自己的黄金法则从不修补受保护的方法。在这种情况下,事情有点平滑,因为保护方法只是为了测试而引入,但我不明白为什么。

这里真正的问题是你不能直接修补datetime.datetime.utcnow(正如你在上面的评论中写的那样是C扩展名)。您可以做的是通过包装标准行为并覆盖datetime函数来修补utcnow

>>> with mock.patch("datetime.datetime", mock.Mock(wraps=datetime.datetime, utcnow=mock.Mock(return_value=3))):
...  print(datetime.datetime.utcnow())
... 
3

好吧,这不是很清楚,但你可以介绍自己的功能,如

def mock_utcnow(return_value):
    return mock.Mock(wraps=datetime.datetime, 
                     utcnow=mock.Mock(return_value=return_value)):

现在

mock.patch("datetime.datetime", mock_utcnow(***))

完全按照您的需要进行操作,不需要任何其他图层,也可以进行各种导入。

另一个解决方案可以在datetime中导入utils并修补***.utils.datetime;这可以让您自由地更改datetime参考实现,而无需更改测试(在这种情况下也要注意更改mock_utcnow() wraps参数。)

答案 1 :(得分:13)

当您修补某个功能的__call__时,您正在设置该实例__call__属性。 Python实际上调用了类中定义的__call__方法。

例如:

>>> class A(object):
...     def __call__(self):
...         print 'a'
...
>>> a = A()
>>> a()
a
>>> def b(): print 'b'
...
>>> b()
b
>>> a.__call__ = b
>>> a()
a
>>> a.__call__ = b.__call__
>>> a()
a

a.__call__分配任何内容都是毫无意义的。

然而:

>>> A.__call__ = b.__call__
>>> a()
b

TLDR;

a()不会致电a.__call__。它会调用type(a).__call__(a)

链接

有一个很好的解释为什么会发生在answer to "Why type(x).__enter__(x) instead of x.__enter__() in Python standard contextlib?"

Special method lookup上的Python文档中记录了此行为。

答案 2 :(得分:4)

正如对这个问题的评论,由于datetime.datetime是用C语言编写的,因此Mock不能替换类中的属性(参见Ned Batchelder的Mocking datetime.today)。相反,您可以使用freezegun

$ pip install freezegun

以下是一个例子:

import datetime

from freezegun import freeze_time

def my_now():
    return datetime.datetime.utcnow()


@freeze_time('2000-01-01 12:00:01')
def test_freezegun():
    assert my_now() == datetime.datetime(2000, 1, 1, 12, 00, 1)

正如您所提到的,另一种方法是跟踪导入datetime的每个模块并对其进行修补。这实际上是 freezegun 的作用。它需要一个模拟datetime的对象,遍历sys.modules以查找已导入datetime的位置并替换每个实例。我想你是否可以在一个功能中优雅地做到这一点是有争议的。