如何为 __getitem__() 调用执行 assert_has_calls?

Lon*_*ner 5 python mocking assertion python-unittest python-3.7

代码:

from unittest.mock import MagicMock, call

mm = MagicMock()
mm().foo()['bar']

print(mm.mock_calls)
print()

mm.assert_has_calls([call(), call().foo(), call().foo().__getitem__('bar')])
Run Code Online (Sandbox Code Playgroud)

输出:

[call(), call().foo(), call().foo().__getitem__('bar')]

Traceback (most recent call last):
  File "foo.py", line 9, in <module>
    mm.assert_has_calls([call(), call().foo(), call().foo().__getitem__('bar')])
TypeError: tuple indices must be integers or slices, not str
Run Code Online (Sandbox Code Playgroud)

如何修复这个断言?

blh*_*ing 8

这是一个错误,因为您应该始终能够使用对象的repr输出call来重新创建具有call相同值的新对象。

这里的问题是call, 的一个实例unittest.mock._Call依赖于__getattr__方法来实现其链式调用注释魔法,_Call当给出一个不存在的属性名称时,返回另一个对象。但是由于_Call是 的子类tuple,它确实定义了__getitem__属性,因此当请求属性时,该_Call.__getattr__方法将简单地返回tuple.__getitem__而不是_Call对象__getitem__。由于tuple.__getitem__不接受字符串作为参数,因此您会收到上述错误。

为了解决这个问题,由于确定是否定义了属性是通过调用__getattribute__方法完成的,AttributeError当找不到给定的属性名称时会引发,我们可以重写,_Call.__getattribute__以便在给定的属性时引发这样的异常name is '__getitem__',有效地使__getitem__“不存在”并将其解析传递给该__getattr__方法,然后该方法将返回一个_Call对象,就像任何其他不存在的属性一样:

def __getattribute__(self, attr):
    if attr == '__getitem__':
        raise AttributeError
    return tuple.__getattribute__(self, attr)

call.__class__.__getattribute__ = __getattribute__ # call.__class__ is _Call
Run Code Online (Sandbox Code Playgroud)

以便:

mm = MagicMock()
mm().foo()['bar']
mm.assert_has_calls([call(), call().foo(), call().foo().__getitem__('bar')])
Run Code Online (Sandbox Code Playgroud)

不会引发任何异常,而:

mm.assert_has_calls([call(), call().foo(), call().foo().__getitem__('foo')])
Run Code Online (Sandbox Code Playgroud)

会提出:

AssertionError: Calls not found.
Expected: [call(), call().foo(), call().foo().__getitem__('foo')]
Actual: [call(), call().foo(), call().foo().__getitem__('bar')]
Run Code Online (Sandbox Code Playgroud)

演示:https : //repl.it/repls/StrikingRedundantAngle

请注意,我已将错误提交到Python 错误跟踪器并将我的修复作为拉取请求提交给 CPython,因此希望您在不久的将来不再需要执行上述操作。