Исправление класса дает "AttributeError: Mock object не имеет атрибута" при доступе к атрибутам экземпляра

Проблема
Использование 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 pass), но когда я пытаюсь выполнить Foo в тестовом примере (проверен с использованием как nosetests, так и pytest) с помощью autospec=True, я сталкиваюсь с "AttributeError: Mock object не имеет атрибута '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 mock для сохранения атрибутов экземпляров Foo?

Примечание:
Это тривиальный пример, который показывает основную проблему. На самом деле, я издеваюсь над сторонним модулем. Class - consul.Consul - клиент которого я создаю в классе оболочки Consul, который у меня есть. Поскольку я не поддерживаю модуль консула, я не могу изменить исходный код в соответствии с моими испытаниями (я бы вообще не хотел этого делать). Для того, что стоит, consul.Consul() возвращает клиент-консул, у которого есть атрибут kv - экземпляр consul.Consul.KV. kv имеет метод get, который я обертываю в методе экземпляра get_key в моем классе Consul. После исправления consul.Consul вызов get не получается из-за AttributeError: объект Mock не имеет атрибута kv.

Ресурсы уже проверены:

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

Ответы

Ответ 1

Нет, автоматическое определение не может макетировать атрибуты, установленные в методе __init__ исходного класса (или в любом другом методе). Он может только макетировать статические атрибуты, все, что можно найти в классе.

В противном случае макет должен был бы создать экземпляр класса, который вы пытались заменить на макет в первую очередь, что не очень хорошая идея (подумайте о классах, которые создают много реальных ресурсов при создании экземпляра).

Тогда рекурсивный характер автоспекционного макета ограничивается этими статическими атрибутами; если foo является атрибутом класса, доступ к Foo().foo вернет автоматически заданный макет для этого атрибута. Если у вас есть класс Spam, чей атрибут eggs является объектом типа Ham, то макет Spam.eggs будет автоматически заданным макетом класса Ham.

Документация, которую вы прочитали, явно описывает это:

Более серьезная проблема заключается в том, что атрибуты экземпляра обычно создаются в методе __init__ и вообще не существуют в классе. autospec не может знать о каких-либо динамически создаваемых атрибутах и ограничивает API видимыми атрибутами.

Вы должны просто установить недостающие атрибуты самостоятельно:

@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()