繁体   English   中英

修补函数的__call__

[英]Patch __call__ of a function

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

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=***):
        ...

但是今天utcnow一个主意,我可以通过修补函数utcnow __call__而不是额外的_utcnow

这对我不起作用:

    from ***.utils import utcnow
    with mock.patch.object(utcnow, '__call__', return_value=***):
        ...

如何优雅地做到这一点?

[编辑]

也许这个问题最有趣的部分是为什么我不能修补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扩展名)。 您可以做的是通过包装标准行为并覆盖utcnow函数来修补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(***))

无需任何其他层和各种导入即可完全满足您的需求。

另一个解决方案是在utils导入datetime并修补***.utils.datetime ; 这样您就可以在不更改测试的情况下自由地更改datetime引用实现(在这种情况下,也请注意更改mock_utcnow() wraps参数)。

修补函数的__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)

链接

对于“为什么在Python标准contextlib中为什么用type(x).__enter__(x)而不是x.__enter__()呢?”有一个很好的解释。

特殊方法查找的 Python文档中记录了此行为。

如对问题的评论所述,由于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导入位置并替换每个实例。 我想是否可以在一个函数中优雅地完成此操作是有争议的。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM