¿Cómo simular una propiedad de solo lectura con simulacro?


92

¿Cómo se burla de una propiedad de solo lectura con simulacro ?

Lo intenté:

setattr(obj.__class__, 'property_to_be_mocked', mock.Mock())

pero el problema es que luego se aplica a todas las instancias de la clase ... lo que rompe mis pruebas.

¿Tiene usted alguna otra idea? No quiero burlarme del objeto completo, solo esta propiedad específica.

Respuestas:


167

Creo que la mejor manera es burlarse de la propiedad como PropertyMock, en lugar de burlarse del __get__método directamente.

Se indica en la documentación , busque unittest.mock.PropertyMock: Un simulacro destinado a ser utilizado como propiedad u otro descriptor en una clase. PropertyMockproporciona __get__y __set__métodos para que pueda especificar un valor de retorno cuando se recupere.

Aquí es cómo:

class MyClass:
    @property
    def last_transaction(self):
        # an expensive and complicated DB query here
        pass

def test(unittest.TestCase):
    with mock.patch('MyClass.last_transaction', new_callable=PropertyMock) as mock_last_transaction:
        mock_last_transaction.return_value = Transaction()
        myclass = MyClass()
        print myclass.last_transaction
        mock_last_transaction.assert_called_once_with()

Tuve que burlarme de un método de clase decorado como @property. Esta respuesta funcionó para mí cuando la otra respuesta (y otras respuestas a muchas otras preguntas) no lo hizo.
AlanSE

3
esta es la forma en que debe hacerse. Ojalá hubiera una manera de mover la respuesta "aceptada"
vitiral

4
Encuentro que incluir el valor de retorno en la llamada del administrador de contexto es un poco más limpio: `` `con mock.patch ('MyClass.last_transaction', new_callable = PropertyMock, return_value = Transaction ()): ...` ``
wodow

De hecho, acabo de mover la respuesta aceptada a esta.
charlax

1
usar mock.patch.object también es bueno ya que no tiene que escribir el nombre de la clase como una cadena (no es realmente un problema en el ejemplo) y es más fácil de detectar / arreglar si decide cambiar el nombre de un paquete y no lo ha hecho actualizado una prueba
Kevin

41

En realidad, la respuesta estaba (como de costumbre) en la documentación , es solo que estaba aplicando el parche a la instancia en lugar de a la clase cuando seguí su ejemplo.

He aquí cómo hacerlo:

class MyClass:
    @property
    def last_transaction(self):
        # an expensive and complicated DB query here
        pass

En la suite de pruebas:

def test():
    # Make sure you patch on MyClass, not on a MyClass instance, otherwise
    # you'll get an AttributeError, because mock is using settattr and
    # last_transaction is a readonly property so there's no setter.
    with mock.patch(MyClass, 'last_transaction') as mock_last_transaction:
        mock_last_transaction.__get__ = mock.Mock(return_value=Transaction())
        myclass = MyClass()
        print myclass.last_transaction

14
la gente debería usar el otro ejemplo. mock.PropertyMockes la forma de hacerlo!
vitiral

4
Eso es correcto, al momento de escribir PropertyMockeste artículo no existía.
charlax

6

Si el objeto cuya propiedad desea anular es un objeto simulado, no tiene que usar patch.

En su lugar, puede crear un PropertyMocky luego anular la propiedad en el tipo de simulacro. Por ejemplo, para anular la mock_rows.pagespropiedad para devolver (mock_page, mock_page,):

mock_page = mock.create_autospec(reader.ReadRowsPage)
# TODO: set up mock_page.
mock_pages = mock.PropertyMock(return_value=(mock_page, mock_page,))
type(mock_rows).pages = mock_pages

1
Bam, justo lo que quería (objeto autoespecificado con una propiedad). Y de un colega nada menos 🙋‍♂️
Mark McDonald

6

Probablemente sea una cuestión de estilo, pero en caso de que prefiera decoradores en las pruebas, la respuesta de @ jamescastlefield podría cambiarse a algo como esto:

class MyClass:
    @property
    def last_transaction(self):
        # an expensive and complicated DB query here
        pass

class Test(unittest.TestCase):
    @mock.patch('MyClass.last_transaction', new_callable=PropertyMock)
    def test(self, mock_last_transaction):
        mock_last_transaction.return_value = Transaction()
        myclass = MyClass()
        print myclass.last_transaction
        mock_last_transaction.assert_called_once_with()

6

En caso de que esté usando pytestjunto con pytest-mock, puede simplificar su código y también evitar usar el administrador de contexto, es decir, la withdeclaración de la siguiente manera:

def test_name(mocker): # mocker is a fixture included in pytest-mock
    mocked_property = mocker.patch(
        'MyClass.property_to_be_mocked',
        new_callable=mocker.PropertyMock,
        return_value='any desired value'
    )
    o = MyClass()

    print(o.property_to_be_mocked) # this will print: any desired value

    mocked_property.assert_called_once_with()

0

Si no desea probar si se accedió o no a la propiedad simulada, simplemente puede parchearla con el archivo esperado return_value.

with mock.patch(MyClass, 'last_transaction', Transaction()):
    ...

0

Si necesita su burla @propertypara confiar en el original __get__, puede crear su personalizadoMockProperty

class PropertyMock(mock.Mock):

    def __get__(self, obj, obj_type=None):
        return self(obj, obj_type)

Uso:

class A:

  @property
  def f(self):
    return 123


original_get = A.f.__get__

def new_get(self, obj_type=None):
  return f'mocked result: {original_get(self, obj_type)}'


with mock.patch('__main__.A.f', new_callable=PropertyMock) as mock_foo:
  mock_foo.side_effect = new_get
  print(A().f)  # mocked result: 123
  print(mock_foo.call_count)  # 1
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.