Affirmation d'appels successifs à une méthode fictive


175

Mock a une méthode utileassert_called_with() . Cependant, pour autant que je sache, cela ne vérifie que le dernier appel à une méthode.
Si j'ai du code qui appelle la méthode simulée 3 fois successivement, à chaque fois avec des paramètres différents, comment puis-je affirmer ces 3 appels avec leurs paramètres spécifiques?

Réponses:


179

assert_has_calls est une autre approche de ce problème.

À partir de la documentation:

assert_has_calls (appels, any_order = False)

assert que le simulacre a été appelé avec les appels spécifiés. La liste mock_calls est vérifiée pour les appels.

Si any_order vaut 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 vaut True, les appels peuvent être dans n'importe quel ordre, mais ils doivent tous apparaître dans mock_calls.

Exemple:

>>> from unittest.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


9
Un peu bizarre, ils ont choisi d'ajouter un nouveau type "appel" pour lequel ils auraient aussi pu simplement utiliser une liste ou un tuple ...
jaapz

@jaapz Il sous tuple- classe : isinstance(mock.call(1), tuple)donne True. Ils ont également ajouté quelques méthodes et attributs.
jpmc26

13
Les premières versions de Mock utilisaient un tuple simple, mais il s'avère difficile à utiliser. Chaque appel de fonction reçoit un tuple de (args, kwargs), donc pour vérifier que "foo (123)" a été appelé correctement, vous devez "assert mock.call_args == ((123,), {})", ce qui est une bouchée comparée à "call (123)"
Jonathan Hartley

Que faites-vous lorsque sur chaque instance d'appel vous attendez une valeur de retour différente?
CodeWithPride

2
@CodeWithPride ça ressemble plus à un travail pourside_effect
Pigueiras

108

Habituellement, je me fiche de l'ordre des appels, mais seulement du fait qu'ils ont eu lieu. Dans ce cas, je combine assert_any_callavec une assertion sur 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 faire de cette façon est plus facile à lire et à comprendre qu'une longue liste d'appels passés dans une seule méthode.

Si vous vous souciez de la commande ou que vous vous attendez à plusieurs appels identiques, cela assert_has_callspourrait être plus approprié.

Éditer

Depuis que j'ai publié cette réponse, j'ai repensé mon approche des tests en général. Je pense qu'il vaut la peine de mentionner que si votre test devient aussi compliqué, vous pouvez le tester de manière inappropriée ou avoir un problème de conception. Les simulacres sont conçus pour tester la communication inter-objets dans une conception orientée objet. Si votre conception n'est pas orientée objet (comme dans plus procédural ou fonctionnel), la maquette peut être totalement inappropriée. Vous pouvez également avoir trop de choses à l'intérieur de la méthode, ou vous pouvez tester des détails internes qu'il vaut mieux ne pas simuler. J'ai développé la stratégie mentionnée dans cette méthode alors que mon code n'était pas très orienté objet, et je crois que je testais également des détails internes qui auraient été mieux laissés non simulés.


@ jpmc26 pouvez-vous en dire plus sur votre modification? Qu'entendez-vous par «mieux ne pas se moquer»? Sinon, comment testeriez-vous si un appel a été effectué dans une méthode
otgw

@memo Souvent, il vaut mieux laisser la vraie méthode être appelée. Si l'autre méthode est défectueuse, cela peut casser le test, mais la valeur d'éviter cela est inférieure à la valeur d'avoir un test plus simple et plus maintenable. Les meilleurs moments pour se moquer sont lorsque l'appel externe à l'autre méthode est ce que vous voulez tester (généralement, cela signifie qu'un type de résultat y est passé et que le code testé ne renvoie pas de résultat.) Ou l'autre méthode a des dépendances externes (base de données, sites Web) que vous souhaitez éliminer. (Techniquement, le dernier cas est plus un bout, et j'hésiterais à l'affirmer.)
jpmc26

@ jpmc26 mocking est utile lorsque vous souhaitez éviter l'injection de dépendances ou une autre méthode de choix de stratégie d'exécution. comme vous l'avez mentionné, tester la logique interne des méthodes, sans appeler de services externes, et plus important encore, sans être conscient de l'environnement (un non non pour un bon code à avoir do() if TEST_ENV=='prod' else dont()), est réalisé facilement en se moquant de la manière que vous avez suggérée. un effet secondaire de cela est de maintenir les tests par versions (disons les changements de code entre Google Search api v1 et v2, votre code testera la version 1 quoi qu'il
arrive

@DanielDubovski La plupart de vos tests doivent être basés sur les entrées / sorties. Ce n'est pas toujours possible, mais si ce n'est pas possible la plupart du temps, vous avez probablement un problème de conception. Lorsque vous avez besoin d'une valeur renvoyée qui provient normalement d'un autre morceau de code et que vous souhaitez couper une dépendance, un stub fera généralement l'affaire. Les simulations ne sont nécessaires que lorsque vous devez vérifier qu'une fonction de modification d'état (probablement sans valeur de retour) est appelée. (La différence entre un faux et un stub est que vous ne vous déclarez pas sur un appel avec un stub.) L'utilisation de simulacres là où les stubs feront l'affaire rend vos tests moins maintenables.
jpmc26

@ jpmc26 n'appelle pas un service externe comme une sorte de sortie? bien sûr, vous pouvez refactoriser le code qui construit le message à envoyer et le tester au lieu d'affirmer les paramètres d'appel, mais à mon humble avis, c'est à peu près la même chose. Comment suggéreriez-vous de reconcevoir les API externes appelantes?
Daniel Dubovski


17

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


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

Supposons que nous ayons une classe résistante (dont 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 HeavyDutyclasse:

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 la heavy_workfonction:

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 HeavyDutyclasse avec MockHeavyDuty. Pour affirmer les appels de méthode provenant de chaque HeavyDutyinstance, nous devons nous référer à MockHeavyDuty.return_value.assert_has_calls, au lieu de MockHeavyDuty.assert_has_calls. De plus, dans la liste des, expected_callsnous devons spécifier le nom de la méthode pour laquelle nous souhaitons affirmer les appels. Donc, notre liste est faite d'appels à call.do_work, plutôt que simplement call.

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

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


Si nous modifions la heavy_workfonction, 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

Pour contraster avec ce qui précède, voici un exemple qui montre comment se moquer de 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 existe deux différences principales. Le premier est que lorsque nous nous moquons d'une fonction, nous configurons nos appels attendus en utilisant callplutôt qu'en utilisant call.some_method. La seconde est que nous appelons assert_has_callsau mock_work_functionlieu de continuer mock_work_function.return_value.

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.