关于Python的:修补类在访问实例属性时会产生“AttributeError:Mock对象没有属性”

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

问题使用mock.patchautospec=True来修补类并不保留该类实例的属性。

细节我试图测试一个类Bar,它将类Foo的一个实例实例化为一个名为FooBar对象属性。测试中的Bar方法称为Bar;它称为Bar实例的Foo方法。在测试中,我嘲笑Foo,因为我只想测试Bar是否访问正确的Foo成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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'

类和方法工作得很好(test_unpatched通过),但是当我尝试在使用autospec=True的测试用例(同时使用nosetest和pytest进行测试)中使用foo时,我遇到"attributeError:mock对象没有属性‘foo’"。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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不在儿童或方法列表中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{'_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 mock以保留foo实例的属性?

注:这是一个显示基本问题的简单示例。实际上,我在模拟第三方模块类consul.Consul,它的客户机是我在一个Consul包装类中实例化的。因为我不维护Consul模块,所以我无法修改源代码以适合我的测试(无论如何我都不想这样做)。就其价值而言,consul.Consul()返回一个具有kv属性的consul客户机,这是consul.Consul.KV的一个实例。kv有一个方法get,我将它包装在Consul类的一个实例方法get_key中。修补consul.Consul后,由于attributeError,get调用失败:mock对象没有属性kv。

已检查的资源:

http://mock.readthedocs.org/en/latest/helpers.html自动排序http://mock.readthedocs.org/en/latest/patch.html


不,autospeccing不能模拟在原始类的__init__方法(或任何其他方法)中设置的属性。它只能模拟静态属性,类中可以找到的所有属性。

否则,mock必须首先创建一个您试图用mock替换的类的实例,这不是一个好主意(想想在实例化时创建大量实际资源的类)。

然后,自动指定的模拟的递归性质仅限于这些静态属性;如果foo是类属性,则访问Foo().foo将返回该属性的自动指定模拟。如果有一个类Spam,其eggs属性是Ham类型的对象,那么Spam.eggs的模拟将是Ham类的自动指定模拟。

您所阅读的文档明确涵盖了以下内容:

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.

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

1
2
3
4
@patch('foo.Foo', autospec=TestFoo)
def test_patched(self, mock_Foo):
    mock_Foo.return_value.foo = 'foo'
    Bar().bar()

或者为测试目的创建foo类的子类,将该属性作为类属性添加:

1
2
3
4
5
6
class TestFoo(foo.Foo):
    foo = 'foo'  # class attribute

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