parchear una clase produce “AttributeError: el objeto simulado no tiene atributo” al acceder a los atributos de la instancia

5 minutos de lectura

Avatar de usuario de Clandestine
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 Foocomo 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=Trueencuentro “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 getque estoy envolviendo en un método de instancia get_key en mi clase de Cónsul. Después de parchear consul.Consulla 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

  • 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

Avatar de usuario de Martijn Pieters
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 Hamentonces 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.

https://docs.python.org/3/library/unittest.mock.html#patch

¿Ha sido útil esta solución?