如何使用MagicMock避免在调用随机方法时出现AttributeError错误?

3

在Python中,如果调用一个不存在的方法,它会抛出AttributeError错误。例如:

>>> class A:
...     def yo(self):
...             print(1)
... 
>>> a = A()
>>> a.yo()
1
>>> a.hello()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'A' object has no attribute 'hello'

在下面的代码中,MagicMock类没有名为hello的函数或未为hello方法创建补丁。尽管如此,下面的代码不会抛出AttributeError错误。
>>> from unittest.mock import MagicMock 
>>> obj = MagicMock()
>>> obj.hello()
<MagicMock name='mock.hello()' id='4408758568'>

MagickMock是如何做到这一点的?我该如何创建一个类,在任何方法(可能未定义)被调用时都可以执行操作?


2
这是魔法。;) - Andras Deak -- Слава Україні
我认为每次调用函数时都会调用一个特殊的方法。而MagicMock会覆盖它。 - 2ank3th
1
模拟文档反复提到这一点;如果你想看到属性错误,你需要规定模拟。 - jonrsharpe
我知道使用autospec=True会抛出AttributeError。我的问题是,MagicMock如何能够返回任何被调用的方法的模拟对象并打开它的对象。 - 2ank3th
2个回答

2
Python数据模型文档了一个钩子__getattr__,当属性访问无法以通常的方式解析时将调用它。Mocks使用它返回一个新的mock实例——即mocks将未知属性定义为工厂
简单地重现mock的实现,你只需将__getattr____call__转换成工厂函数:
class M:
    def __call__(self):
        return M()
    def __getattr__(self, name):
        return M()

使用示例:

>>> mock = M()
>>> mock.potato
<__main__.M at 0xdeadbeef>
>>> mock.potato()
<__main__.M at 0xcafef00d>

"MagicMock是如何做到这一点的?"
这部分内容不仅适用于'MagicMock',普通的'Mock'也会有相同的效果(名称中的“magic”只是指额外的功能,允许更好地模拟魔法方法)。MagicMock 从其中一个基类继承了这种行为。"
>>> MagicMock.mro()
[unittest.mock.MagicMock,
 unittest.mock.MagicMixin,
 unittest.mock.Mock,
 unittest.mock.CallableMixin,
 unittest.mock.NonCallableMock,  # <--- this one overrides __getattr__!
 unittest.mock.Base,
 object]

如何创建一个类,使其在调用任何方法(可能未定义)时执行某个操作?
这取决于您想在一个普通属性访问的前面还是后面执行该操作。如果您想在前面执行该操作,您应该定义__getattribute__,它会在搜索类/实例命名空间之前无条件调用以实现属性访问。然而,如果您想对普通属性(即存在于对象__dict__中的属性)和描述符采取较低优先级,则应像以前讨论的那样定义__getattr__

Mock或MagicMock本身是否支持对象的任何方法调用的前/后行为?我有一个函数,保存了我的mock对象调用的响应,并希望该mock上的任何调用都能以该函数的结果完成。 - LeanMan

1

我其实不知道 MagicMock 具体是如何工作的(我从未使用过,但我听说它很好用),但是这种行为可以通过劫持 __getattr__ 的方式来复制(以及可能有多种其他解决方案),使其返回一个可调用对象,在调用时创建一个新的模拟实例:

class MM:
    def __init__(self, name=None):
        # store a name, TODO: random id, etc.
        self.name = name

    def __repr__(self):
        # make it pretty
        if self.name:
            r = f'<MM name={self.name}>'
        else:
            r = f'<MM>'
        return r

    def __getattr__(self, attrname):
        # we want a factory for a mock instance with a name corresponding to attrname
        def magicattr():
            return MM(name=f"'mock.{attrname}()'")
        return magicattr

执行时,我们会看到以下内容:
>>> MM()
<MM>
>>> MM().hello()
<MM name='mock.hello()'>

我没有过度定义id等内容,但基本技巧可以在上面简化的示例中看到。
上述方法的工作原理是访问.hello或任何其他属性都经过我们自定义的__getattr__,这使我们有机会即时生成一个带有我们想要的任何属性的虚假(模拟)方法。据我所知,MagicMock的许多好处之一正是我们不必担心默认情况下会抛出AttributeError,它只是起作用。

1
这正是我正在寻找的,谢谢! - 2ank3th
2
@2ank3th,我建议将接受答案更改为wim的回答。我只发了一个帖子,因为对这个主题更熟悉的人似乎没有回应。 - Andras Deak -- Слава Україні

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接