¿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.

charlax
fuente

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()
jamescastlefield
fuente
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
charlax
fuente
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
Tim Swast
fuente
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()
Eyal Levin
fuente
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()
lmiguelvargasf
fuente
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()):
    ...
Simon Charette
fuente
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
Conchylicultor
fuente