patching a class yields "AttributeError: Mock object has no attribute" when accessing instance attributes

patching a class yields "AttributeError: Mock object has no attribute" when accessing instance attributes

问题
使用 mock.patchautospec=True 来修补 class 不会保留 class.

实例的属性

详情
我正在尝试测试将 class Foo 的实例实例化为名为 fooBar 对象属性的 class Bar。被测Bar方法调用bar;它调用属于 BarFoo 实例的方法 foo。在测试这个时,我在嘲笑 Foo,因为我只想测试 Bar 正在访问正确的 Foo 成员:

import unittest
from mock import patch

class Foo(object):
    def __init__(self):
        self.foo = 'foo'

class Bar(object):
    def __init__(self):
        self.foo = Foo()

    def bar(self):
        return self.foo.foo

class TestBar(unittest.TestCase):
    @patch('foo.Foo', autospec=True)
    def test_patched(self, mock_Foo):
        Bar().bar()

    def test_unpatched(self):
        assert Bar().bar() == 'foo'

classes 和方法工作得很好(test_unpatched 通过),但是当我尝试在测试用例中使用 Foo(同时使用 nosetests 和 pytest 进行测试)时使用 autospec=True ,我遇到了"AttributeError: Mock object has no attribute 'foo'"

19:39 $ nosetests -sv foo.py
test_patched (foo.TestBar) ... ERROR
test_unpatched (foo.TestBar) ... ok

======================================================================
ERROR: test_patched (foo.TestBar)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/dist-packages/mock.py", line 1201, in patched
    return func(*args, **keywargs)
  File "/home/vagrant/dev/constellation/test/foo.py", line 19, in test_patched
    Bar().bar()
  File "/home/vagrant/dev/constellation/test/foo.py", line 14, in bar
    return self.foo.foo
  File "/usr/local/lib/python2.7/dist-packages/mock.py", line 658, in __getattr__
    raise AttributeError("Mock object has no attribute %r" % name)
AttributeError: Mock object has no attribute 'foo'

确实,当我打印出 mock_Foo.return_value.__dict__ 时,我可以看到 foo 不在子列表或方法列表中:

{'_mock_call_args': None,
 '_mock_call_args_list': [],
 '_mock_call_count': 0,
 '_mock_called': False,
 '_mock_children': {},
 '_mock_delegate': None,
 '_mock_methods': ['__class__',
                   '__delattr__',
                   '__dict__',
                   '__doc__',
                   '__format__',
                   '__getattribute__',
                   '__hash__',
                   '__init__',
                   '__module__',
                   '__new__',
                   '__reduce__',
                   '__reduce_ex__',
                   '__repr__',
                   '__setattr__',
                   '__sizeof__',
                   '__str__',
                   '__subclasshook__',
                   '__weakref__'],
 '_mock_mock_calls': [],
 '_mock_name': '()',
 '_mock_new_name': '()',
 '_mock_new_parent': <MagicMock name='Foo' spec='Foo' id='38485392'>,
 '_mock_parent': <MagicMock name='Foo' spec='Foo' id='38485392'>,
 '_mock_wraps': None,
 '_spec_class': <class 'foo.Foo'>,
 '_spec_set': None,
 'method_calls': []}

我对 autospec 的理解是,如果为真,补丁规范应该递归应用。既然 foo 确实是 Foo 实例的一个属性,难道不应该打补丁吗?如果没有,我如何获得 Foo 模拟以保留 Foo 实例的属性?

注意:
这是一个显示基本问题的简单示例。实际上,我在嘲笑第三方 module.Class -- consul.Consul -- 我在我拥有的 Consul 包装器 class 中实例化了它的客户端。由于我不维护 consul 模块,因此我无法修改源代码以适合我的测试(无论如何我真的不想这样做)。对于它的价值,consul.Consul() returns 一个 consul 客户端,它有一个属性 kv -- consul.Consul.KV 的一个实例。 kv 有一个方法 get,我将其包装在我的 Consul class 中的一个实例方法 get_key 中。打补丁后consul.Consul,调用get失败,原因是AttributeError: Mock object has no attribute kv.

已检查的资源:

http://mock.readthedocs.org/en/latest/helpers.html#autospeccing http://mock.readthedocs.org/en/latest/patch.html

不,autospeccing 无法模拟原始 class 的 __init__ 方法(或任何其他方法)中设置的属性。它只能mock出静态属性,在class.

上能找到的一切

否则,mock 将不得不创建一个 class 的实例,您首先尝试用 mock 替换它,这不是一个好主意(想想 classes 创建实例化时有很多实际资源)。

auto-specced mock 的递归性质随后仅限于那些静态属性;如果 foo 是一个 class 属性,访问 Foo().foo 将 return 该属性的自动指定模拟。如果你有一个 class Spameggs 属性是类型 Ham 的对象,那么 Spam.eggs 的模拟将是自动指定的模拟Ham class.

documentation you read明确涵盖了这个:

A more serious problem is that it is common for instance attributes to be created in the __init__ method and not to exist on the class at all. autospec can’t know about any dynamically created attributes and restricts the api to visible attributes.

您应该自己设置缺少的属性:

@patch('foo.Foo', autospec=Foo)
def test_patched(self, mock_Foo):
    mock_Foo.return_value.foo = 'foo'
    Bar().bar()

或创建 Foo class 的子class 用于测试目的,将属性添加为 class 属性:

class TestFoo(foo.Foo):
    foo = 'foo'  # class attribute

@patch('foo.Foo', autospec=TestFoo)
def test_patched(self, mock_Foo):
    Bar().bar()

patch 中只有一个 create kwarg,当设置为 True 时,如果该属性尚不存在,将创建该属性。

If you pass in create=True, and the attribute doesn’t exist, patch will create the attribute for you when the patched function is called, and delete it again after the patched function has exited.

https://docs.python.org/3/library/unittest.mock.html#patch