web-dev-qa-db-ja.com

クラスにパッチを適用すると、インスタンス属性にアクセスするときに「AttributeError:Mock object has no attribute」が生成されます

問題
_mock.patch_を_autospec=True_と組み合わせてクラスにパッチを適用しても、そのクラスのインスタンスの属性は保持されません。

詳細
クラスBarのインスタンスをFooと呼ばれるBarオブジェクト属性としてインスタンス化するクラスfooをテストしようとしています。テスト中のBarメソッドはbarと呼ばれます。 fooに属するFooインスタンスのメソッドBarを呼び出します。これをテストするとき、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'
_

クラスとメソッドは正常に機能しますが(_test_unpatched_合格)、_autospec=True_を使用してテスト(ノーズテストとpytestの両方を使用してテスト)でFooを実行しようとすると、「AttributeError:Mock object has no属性「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についての私の理解では、Trueの場合、パッチの仕様は再帰的に適用されるはずです。 fooは確かにFooインスタンスの属性であるため、パッチを適用すべきではありませんか?そうでない場合、Fooインスタンスの属性を保持するためにFooモックを取得するにはどうすればよいですか?

注:
これは、基本的な問題を示す簡単な例です。実際には、サードパーティのモジュールのモックを作成しています。クラス-_consul.Consul_-そのクライアントは、私が持っているConsulラッパークラスでインスタンス化します。私はconsulモジュールを維持していないので、テストに合わせてソースを変更することはできません(とにかくそれをしたくありません)。価値があるものとして、consul.Consul()は、属性kv-_consul.Consul.KV_のインスタンスを持つconsulクライアントを返します。 kvにはメソッドgetがあり、Consulクラスのインスタンスメソッド_get_key_にラップしています。 _consul.Consul_にパッチを適用した後、AttributeError:Mockオブジェクトに属性kvがないため、getの呼び出しが失敗します。

すでにチェックされているリソース:

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

33
Clandestine

いいえ、自動スペシングは元のクラスの___init___メソッド(または他のメソッド)で設定された属性をモックアウトできません。クラスで見つけることができるすべての静的属性のみをモックアウトできます。

そうしないと、モックは最初にモックで置き換えようとしたクラスのインスタンスを作成する必要がありますが、これは良い考えではありません(インスタンス化時に多くの実リソースを作成するクラスを考えてください)。

自動指定されたモックの再帰的な性質は、これらの静的属性に限定されます。 fooがクラス属性の場合、Foo().fooにアクセスすると、その属性の自動指定モックが返されます。 Spam属性がeggs型のオブジェクトであるクラスHamがある場合、_Spam.eggs_のモックはHamクラスの自動指定モックになります。

あなたが読んだドキュメントexplicitlyはこれをカバーしています:

より深刻な問題は、インスタンス属性が___init___メソッドで作成され、クラスにまったく存在しないことが一般的であることです。 autospecは動的に作成された属性を認識できず、APIを表示可能な属性に制限します。

不足している属性を自分でsetするだけです:

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

または、属性をクラス属性として追加するテスト目的でFooクラスのサブクラスを作成します。

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

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