web-dev-qa-db-fra.com

Affirmation d'appels successifs à une méthode fictive

Mock a une méthode tile assert_called_with()) . Cependant, pour autant que je sache, ceci ne fait que vérifier le dernier appel d'une méthode.
Si j'ai un code qui appelle la méthode simulée 3 fois de suite, à chaque fois avec des paramètres différents, comment puis-je affirmer ces 3 appels avec leurs paramètres spécifiques?

130
Jonathan

Vous pouvez utiliser le Mock.call_args_list attribut pour comparer les paramètres aux appels de méthode précédents. Cela conjointement avec Mock.call_count _ attribut devrait vous donner le contrôle total.

38
Jonathan

assert_has_calls est une autre approche de ce problème.

De la docs:

assert_has_calls (appels, any_order = False)

affirmer que la maquette a été appelée avec les appels spécifiés. La liste d'appels mock_calls est vérifiée.

Si any_order a la valeur False (valeur par défaut), les appels doivent être séquentiels. Il peut y avoir des appels supplémentaires avant ou après les appels spécifiés.

Si any_order est True, les appels peuvent être dans n'importe quel ordre, mais ils doivent tous apparaître dans mock_calls.

Exemple:

>>> from mock import call, Mock
>>> mock = Mock(return_value=None)
>>> mock(1)
>>> mock(2)
>>> mock(3)
>>> mock(4)
>>> calls = [call(2), call(3)]
>>> mock.assert_has_calls(calls)
>>> calls = [call(4), call(2), call(3)]
>>> mock.assert_has_calls(calls, any_order=True)

Source: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_has_calls

135
Pigueiras

Habituellement, peu m'importe l'ordre des appels, seulement qu'ils se soient produits. Dans ce cas, je combine assert_any_call avec une assertion à propos de call_count .

>>> import mock
>>> m = mock.Mock()
>>> m(1)
<Mock name='mock()' id='37578160'>
>>> m(2)
<Mock name='mock()' id='37578160'>
>>> m(3)
<Mock name='mock()' id='37578160'>
>>> m.assert_any_call(1)
>>> m.assert_any_call(2)
>>> m.assert_any_call(3)
>>> assert 3 == m.call_count
>>> m.assert_any_call(4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "[python path]\lib\site-packages\mock.py", line 891, in assert_any_call
    '%s call not found' % expected_string
AssertionError: mock(4) call not found

Je trouve que le faire de cette façon est plus facile à lire et à comprendre qu’une longue liste d’appels passés à une seule méthode.

Si vous vous souciez de la commande ou si vous attendez plusieurs appels identiques, assert_has_calls pourrait être plus approprié.

Modifier

Depuis que j'ai posté cette réponse, j'ai repensé mon approche des tests en général. Je pense qu'il convient de mentionner que si votre test devient aussi compliqué, vous pouvez effectuer des tests de manière inappropriée ou avoir un problème de conception. Les simulacres sont conçus pour tester la communication inter-objet dans une conception orientée objet. Si votre conception n'est pas orientée objectivement (comme dans les procédures ou les fonctions), la maquette peut être totalement inappropriée. Il se peut également que trop de choses se passent dans la méthode ou que vous testiez des détails internes qu'il vaut mieux ne pas modifier. J'ai développé la stratégie mentionnée dans cette méthode lorsque mon code n'était pas très orienté objet, et je crois que je testais également des détails internes qui auraient mieux été laissés tels quels.

85
jpmc26

Je dois toujours regarder celui-ci maintes et maintes fois, alors voici ma réponse.


Assertion de plusieurs appels de méthode sur différents objets de la même classe

Supposons que nous ayons une classe de poids lourds (que nous voulons nous moquer):

In [1]: class HeavyDuty(object):
   ...:     def __init__(self):
   ...:         import time
   ...:         time.sleep(2)  # <- Spends a lot of time here
   ...:     
   ...:     def do_work(self, arg1, arg2):
   ...:         print("Called with %r and %r" % (arg1, arg2))
   ...:  

voici du code qui utilise deux instances de la classe HeavyDuty:

In [2]: def heavy_work():
   ...:     hd1 = HeavyDuty()
   ...:     hd1.do_work(13, 17)
   ...:     hd2 = HeavyDuty()
   ...:     hd2.do_work(23, 29)
   ...:    


Maintenant, voici un cas de test pour le heavy_work une fonction:

In [3]: from unittest.mock import patch, call
   ...: def test_heavy_work():
   ...:     expected_calls = [call.do_work(13, 17),call.do_work(23, 29)]
   ...:     
   ...:     with patch('__main__.HeavyDuty') as MockHeavyDuty:
   ...:         heavy_work()
   ...:         MockHeavyDuty.return_value.assert_has_calls(expected_calls)
   ...:  

Nous nous moquons de la classe HeavyDuty avec MockHeavyDuty. Pour affirmer des appels de méthode provenant de chaque instance HeavyDuty, nous devons nous référer à MockHeavyDuty.return_value.assert_has_calls, au lieu de MockHeavyDuty.assert_has_calls. De plus, dans la liste de expected_calls nous devons spécifier le nom de la méthode pour laquelle nous souhaitons appliquer des appels. Notre liste est donc composée d'appels à call.do_work, par opposition à simplement call.

L'exercice du cas de test nous montre qu'il a réussi:

In [4]: print(test_heavy_work())
None


Si nous modifions le heavy_work _ fonction, le test échoue et produit un message d'erreur utile:

In [5]: def heavy_work():
   ...:     hd1 = HeavyDuty()
   ...:     hd1.do_work(113, 117)  # <- call args are different
   ...:     hd2 = HeavyDuty()
   ...:     hd2.do_work(123, 129)  # <- call args are different
   ...:     

In [6]: print(test_heavy_work())
---------------------------------------------------------------------------
(traceback omitted for clarity)

AssertionError: Calls not found.
Expected: [call.do_work(13, 17), call.do_work(23, 29)]
Actual: [call.do_work(113, 117), call.do_work(123, 129)]


Affirmer plusieurs appels à une fonction

Par opposition à ce qui précède, voici un exemple qui montre comment simuler plusieurs appels à une fonction:

In [7]: def work_function(arg1, arg2):
   ...:     print("Called with args %r and %r" % (arg1, arg2))

In [8]: from unittest.mock import patch, call
   ...: def test_work_function():
   ...:     expected_calls = [call(13, 17), call(23, 29)]    
   ...:     with patch('__main__.work_function') as mock_work_function:
   ...:         work_function(13, 17)
   ...:         work_function(23, 29)
   ...:         mock_work_function.assert_has_calls(expected_calls)
   ...:    

In [9]: print(test_work_function())
None


Il y a deux différences principales. La première est que lorsque nous moquons une fonction, nous configurons nos appels attendus en utilisant call, au lieu d’utiliser call.some_method. La seconde est que nous appelons assert_has_calls sur mock_work_function, au lieu de mock_work_function.return_value.

16
Pedro M Duarte