parcheando una clase produce “AttributeError: el objeto simulado no tiene ningún atributo” al acceder a los atributos de instancia

El problema
El uso de mock.patch con autospec=True para parchear una clase no preserva los atributos de las instancias de esa clase.

Los detalles
Estoy tratando de probar una Bar clase que crea una instancia de la clase Foo como un atributo de objeto de Bar llamado foo . El método de Bar bajo prueba se llama bar ; llama al método foo de la instancia de Foo pertenece a Bar . Al probar esto, me estoy burlando de Foo , ya que solo quiero probar que Bar está accediendo al miembro de Foo correcto:

 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' 

Las clases y los métodos funcionan bien (pases test_unpatched ), pero cuando trato de Foo en un caso de prueba (probado usando tanto nosestests como pytest) usando autospec=True , encuentro “AttributeError: el objeto autospec=True no tiene el atributo ‘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' 

De hecho, cuando mock_Foo.return_value.__dict__ , puedo ver que foo no está en la lista de hijos o métodos:

 {'_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': , '_mock_parent': , '_mock_wraps': None, '_spec_class': , '_spec_set': None, 'method_calls': []} 

Mi comprensión de autospec es que, si es True, las especificaciones del parche deberían aplicarse recursivamente. Dado que foo es de hecho un atributo de las instancias de Foo, ¿no debería ser parcheado? Si no, ¿cómo obtengo la simulación de Foo para preservar los atributos de las instancias de Foo?

NOTA:
Este es un ejemplo trivial que muestra el problema básico. En realidad, me estoy burlando de un módulo de terceros. Class – consul.Consul – cuyo cliente consul.Consul en una clase de envoltorio de cónsul que tengo. Como no mantengo el módulo de cónsul, no puedo modificar la fuente para que se adapte a mis pruebas (de todos modos no querría hacer eso). Para lo que vale, consul.Consul() devuelve un cliente de cónsul, que tiene un atributo kv , una instancia de consul.Consul.KV . kv tiene un método get , que estoy envolviendo en un método de instancia get_key en mi clase Cónsul. Después de parchear el consul.Consul , la llamada a obtener falla debido a AttributeError: El objeto consul.Consul no tiene ningún atributo kv.

Recursos ya revisados:

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

No, la exploración automática no puede imitar los atributos establecidos en el método __init__ de la clase original (o en cualquier otro método). Solo puede simular atributos estáticos , todo lo que se puede encontrar en la clase.

De lo contrario, el simulacro tendría que crear una instancia de la clase que intentó reemplazar con un simulacro en primer lugar, lo cual no es una buena idea (piense en las clases que crean una gran cantidad de recursos reales cuando se crea una instancia).

La naturaleza recursiva de un simulacro de especificación automática se limita a esos atributos estáticos; si foo es un atributo de clase, al acceder a Foo().foo devolverá una simulación auto-especificada para ese atributo. Si tiene una clase de Spam cuyo atributo de eggs es un objeto de tipo Ham , entonces el simulacro de Spam.eggs será una simulación auto-especificada de la clase de Ham .

La documentación que lea explícitamente cubre esto:

Un problema más serio es que es común que los atributos de instancia se creen en el método __init__ y no existan en la clase. autospec no puede conocer los atributos creados dinámicamente y restringe la API a los atributos visibles.

Solo debes establecer los atributos que faltan:

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

o cree una subclase de su clase Foo para propósitos de prueba que agregue el atributo como un atributo de clase:

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