Исправление класса дает "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()