Comment se moquer d'une propriété en lecture seule avec une maquette?


92

Comment vous moquez-vous d'une propriété en lecture seule avec une simulation ?

J'ai essayé:

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

mais le problème est qu'il s'applique ensuite à toutes les instances de la classe ... ce qui rompt mes tests.

Avez-vous une autre idée? Je ne veux pas me moquer de l'objet complet, seulement cette propriété spécifique.

Réponses:


166

Je pense que le meilleur moyen est de se moquer de la propriété PropertyMockplutôt que de se moquer __get__directement de la méthode.

Il est indiqué dans la documentation , recherchez unittest.mock.PropertyMock: Une maquette destinée à être utilisée comme propriété, ou autre descripteur, sur une classe. PropertyMockfournit __get__et des __set__méthodes afin que vous puissiez spécifier une valeur de retour lors de sa récupération.

Voici comment:

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()

J'ai dû me moquer d'une méthode de classe décorée comme @property. Cette réponse a fonctionné pour moi alors que l'autre réponse (et d'autres réponses à de nombreuses autres questions) n'a pas fonctionné.
AlanSE

3
c'est ainsi que cela doit être fait. Je souhaite qu'il y ait un moyen de déplacer la réponse "acceptée"
vitiral

4
Je trouve que l'inclusion de la valeur de retour dans l'appel du gestionnaire de contexte est légèrement plus propre: `` `` avec mock.patch ('MyClass.last_transaction', new_callable = PropertyMock, return_value = Transaction ()): ... `` ``
wodow

En effet, je viens de déplacer la réponse acceptée vers celle-ci.
charlax

1
l'utilisation de mock.patch.object est également intéressante car vous n'avez pas à écrire le nom de la classe sous forme de chaîne (ce n'est pas vraiment un problème dans l'exemple) et il est plus facile de détecter / corriger si vous décidez de renommer un package et que vous ne l'avez pas fait a mis à jour un test
Kevin

41

En fait, la réponse était (comme d'habitude) dans la documentation , c'est juste que j'appliquais le correctif à l'instance au lieu de la classe lorsque j'ai suivi leur exemple.

Voici comment faire:

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

Dans la suite de tests:

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
les gens devraient utiliser l'autre exemple. mock.PropertyMockest le moyen de le faire!
vitiral

4
C'est exact, au moment de la rédaction de cet article PropertyMockn'existait pas.
charlax

6

Si l'objet dont vous souhaitez remplacer la propriété est un objet fictif, vous n'avez pas à l'utiliser patch.

Au lieu de cela, peut créer un PropertyMock, puis remplacer la propriété sur le type de la maquette. Par exemple, pour remplacer la mock_rows.pagespropriété à renvoyer (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, exactement ce que je voulais (objet autospec'd avec une propriété). Et d'un collègue pas moins 🙋‍♂️
Mark McDonald

6

Probablement une question de style, mais au cas où vous préférez les décorateurs dans les tests, la réponse de @ jamescastlefield pourrait être changée en quelque chose comme ceci:

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

Dans le cas où vous utilisez pytestavec pytest-mock, vous pouvez simplifier votre code et également éviter d'utiliser le gestionnaire de contexte, c'est-à-dire l' withinstruction comme suit:

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 vous ne voulez pas tester si la propriété simulée a été accédée ou non, vous pouvez simplement la patcher avec le fichier attendu return_value.

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

0

Si vous avez besoin que votre simulé @propertys'appuie sur l'original __get__, vous pouvez créer votre personnaliséMockProperty

class PropertyMock(mock.Mock):

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

Usage:

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
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.