Clandestino
El problema
Usando mock.patch
con autospec=True
parchear una clase no preserva los atributos de las instancias de esa clase.
Los detalles
Estoy tratando de probar una clase. Bar
que instancia una instancia de clase Foo
como un Bar
atributo de objeto llamado foo
. los Bar
El método bajo prueba se llama bar
; llama método foo
del Foo
instancia perteneciente a Bar
. Al probar esto, me estoy burlando Foo
como solo quiero probar eso Bar
está accediendo a la correcta Foo
miembro:
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 (test_unpatched
pasa), pero cuando trato de Foo en un caso de prueba (probado usando ambos nosetests y pytest) usando autospec=True
encuentro “AttributeError: el objeto simulado 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 imprimo mock_Foo.return_value.__dict__
Puedo ver eso foo
no está en la lista de niños 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': <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': []}
Mi comprensión de las especificaciones automáticas es que, si es Verdadero, las especificaciones del parche deberían aplicarse de forma recursiva. Dado que foo es de hecho un atributo de las instancias de Foo, ¿no debería parchearse? Si no, ¿cómo obtengo el simulacro de Foo para conservar 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. Clase — consul.Consul
— cuyo cliente instalo en una clase contenedora Consul que tengo. Como no mantengo el módulo de cónsul, no puedo modificar la fuente para adaptarla a mis pruebas (realmente no me gustaría hacer eso de todos modos). Por lo que vale, consul.Consul()
devuelve un cliente 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 de Cónsul. Después de parchear consul.Consul
la llamada para obtener falla debido a AttributeError: el objeto simulado no tiene atributo kv.
Recursos ya comprobados:
http://mock.readthedocs.org/en/latest/helpers.html#autospeccing
http://mock.readthedocs.org/en/latest/patch.html
Martijn Pieters
No, la especificación automática no puede imitar los atributos establecidos en el __init__
método de la clase original (o en cualquier otro método). Solo puede burlarse atributos estáticostodo 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 clases que crean muchos recursos reales cuando se instancian).
La naturaleza recursiva de un simulacro con especificaciones automáticas se limita entonces a esos atributos estáticos; si foo
es un atributo de clase, accediendo Foo().foo
devolverá un simulacro especificado automáticamente para ese atributo. si tienes una clase Spam
cuyo eggs
atributo es un objeto de tipo Ham
entonces el simulacro de Spam.eggs
será un simulacro con especificaciones automáticas del Ham
clase.
los documentación que lees explícitamente cubre esto:
Un problema más serio es que es común que los atributos de instancia se creen en el
__init__
método y no existir en la clase en absoluto.autospec
no puede conocer ningún atributo creado dinámicamente y restringe la API a atributos visibles.
solo deberías establecer los atributos que faltan usted mismo:
@patch('foo.Foo', autospec=Foo)
def test_patched(self, mock_Foo):
mock_Foo.return_value.foo = 'foo'
Bar().bar()
o crea una subclase de tu Foo
class con fines de prueba que agrega 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()
Solo hay un kwarg de creación en el parche que, cuando se establece en Verdadero, creará el atributo si aún no existe.
Si pasa create=True y el atributo no existe, patch creará el atributo por usted cuando se llame a la función parcheada y lo eliminará nuevamente después de que la función parcheada haya salido.
Eso requeriría el simulacro para crear una instancia de la clase. Eso nunca es una buena idea, porque eso requeriría ejecutar el código que estaba tratando de reemplazar con un simulacro en primer lugar.
– Martijn Pieters
♦
29 de julio de 2015 a las 19:57
Comentario secundario no relacionado, pero esta pregunta se hace de una manera realmente hermosa. Me encanta cómo el OP describe su situación, lo que espera, muestra y ejemplifica y luego explica su situación del mundo real seguida de las referencias que ha visto. Prestigio
– Greg Hilton
9 de junio de 2021 a las 17:06