Aktivieren aufeinanderfolgender Aufrufe einer Scheinmethode


175

Mock hat eine hilfreiche assert_called_with()Methode . Soweit ich weiß, überprüft dies jedoch nur den letzten Aufruf einer Methode.
Wenn ich Code habe, der die verspottete Methode dreimal hintereinander aufruft, jedes Mal mit unterschiedlichen Parametern, wie kann ich diese drei Aufrufe mit ihren spezifischen Parametern bestätigen?

Antworten:


179

assert_has_calls ist ein weiterer Ansatz für dieses Problem.

Aus den Dokumenten:

assert_has_calls (Aufrufe, any_order = False)

Bestätigen Sie, dass der Mock mit den angegebenen Aufrufen aufgerufen wurde. Die Liste mock_calls wird auf Aufrufe überprüft.

Wenn any_order False ist (Standardeinstellung), müssen die Aufrufe sequentiell sein. Vor oder nach den angegebenen Anrufen können zusätzliche Anrufe erfolgen.

Wenn any_order True ist, können die Aufrufe in beliebiger Reihenfolge erfolgen, sie müssen jedoch alle in mock_calls angezeigt werden.

Beispiel:

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

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


9
Ein bisschen komisch, dass sie einen neuen "Anruf" -Typ hinzugefügt haben, für den sie auch nur eine Liste oder ein Tupel hätten verwenden können ...
jaapz

@jaapz Es Unterklassen tuple: isinstance(mock.call(1), tuple)gibt True. Sie fügten auch einige Methoden und Attribute hinzu.
jpmc26

13
Frühe Versionen von Mock verwendeten ein einfaches Tupel, aber es stellt sich als umständlich heraus, es zu verwenden. Jeder Funktionsaufruf erhält ein Tupel von (args, kwargs). Um zu überprüfen, ob "foo (123)" korrekt aufgerufen wurde, müssen Sie "mock.call_args == ((123,), {})" aktivieren ein Schluck im Vergleich zu "call (123)"
Jonathan Hartley

Was tun Sie, wenn Sie bei jeder Anrufinstanz einen anderen Rückgabewert erwarten?
CodeWithPride

2
@ CodeWithPride es sieht eher nach einem Job ausside_effect
Pigueiras

108

Normalerweise ist mir die Reihenfolge der Anrufe egal, nur dass sie passiert sind. In diesem Fall verbinde ich mich assert_any_callmit einer Behauptung über 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

Ich finde es einfacher zu lesen und zu verstehen als eine große Liste von Aufrufen, die an eine einzelne Methode übergeben werden.

Wenn Sie sich für die Bestellung interessieren oder mehrere identische Anrufe erwarten, ist dies assert_has_callsmöglicherweise besser geeignet.

Bearbeiten

Seit ich diese Antwort gepostet habe, habe ich meinen Testansatz im Allgemeinen überdacht. Ich denke, es ist erwähnenswert, dass Sie möglicherweise unangemessen testen oder ein Designproblem haben, wenn Ihr Test so kompliziert wird. Mocks dienen zum Testen der Kommunikation zwischen Objekten in einem objektorientierten Design. Wenn Ihr Design nicht objektorientiert ist (wie bei mehr prozeduralen oder funktionalen), ist das Modell möglicherweise völlig unangemessen. Möglicherweise ist in der Methode auch zu viel los, oder Sie testen interne Details, die am besten nicht verspottet werden. Ich habe die in dieser Methode erwähnte Strategie entwickelt, als mein Code nicht sehr objektorientiert war, und ich glaube, ich habe auch interne Details getestet, die am besten nicht verspottet worden wären.


@ jpmc26 Könntest du mehr über deine Bearbeitung erfahren? Was meinst du mit "am besten nicht verspottet"? Wie sonst würden Sie testen, ob ein Aufruf innerhalb einer Methode
getätigt wurde

@memo Oft ist es besser, die reale Methode aufrufen zu lassen. Wenn die andere Methode fehlerhaft ist, wird der Test möglicherweise unterbrochen, aber der Wert der Vermeidung ist kleiner als der Wert eines einfacheren, wartbareren Tests. Die besten Zeiten zum Verspotten sind, wenn der externe Aufruf der anderen Methode das ist, was Sie testen möchten (normalerweise bedeutet dies, dass eine Art Ergebnis an sie übergeben wird und der zu testende Code kein Ergebnis zurückgibt.) Oder die andere Methode hat externe Abhängigkeiten (Datenbank, Websites), die Sie beseitigen möchten. (Technisch gesehen ist der letzte Fall eher ein Stummel, und ich würde zögern, dies zu behaupten.)
jpmc26

@ jpmc26 Mocking ist nützlich, wenn Sie die Abhängigkeitsinjektion oder eine andere Methode zur Auswahl der Laufzeitstrategie vermeiden möchten. Wie Sie bereits erwähnt haben, lässt sich das Testen der inneren Logik von Methoden, ohne Aufrufen externer Dienste und vor allem ohne Umgebungsbewusstsein (ein Nein Nein für guten Code do() if TEST_ENV=='prod' else dont()), leicht erreichen, indem Sie sich über die von Ihnen vorgeschlagene Art und Weise lustig machen. Ein Nebeneffekt davon ist die Aufrechterhaltung von Tests pro Version (sagen wir, Codeänderungen zwischen Google Search API v1 und v2, Ihr Code wird Version 1 testen, egal was
passiert

@ DanielDubovski Die meisten Ihrer Tests sollten auf Eingabe / Ausgabe basieren. Das ist nicht immer möglich, aber wenn es die meiste Zeit nicht möglich ist, haben Sie wahrscheinlich ein Designproblem. Wenn Sie einen Wert zurückgeben müssen, der normalerweise von einem anderen Code stammt, und eine Abhängigkeit entfernen möchten, reicht normalerweise ein Stub aus. Mocks sind nur erforderlich, wenn Sie überprüfen müssen, ob eine Statusänderungsfunktion (wahrscheinlich ohne Rückgabewert) aufgerufen wird. (Der Unterschied zwischen einem Mock und einem Stub besteht darin, dass Sie bei einem Anruf mit einem Stub keinen Anspruch erheben.) Die Verwendung von Mocks, bei denen Stubs ausgeführt werden, macht Ihre Tests weniger wartbar.
jpmc26

@ jpmc26 ruft ein externer Dienst nicht als eine Art Ausgabe auf? Natürlich können Sie den Code, der die zu sendende Nachricht erstellt, umgestalten und testen, anstatt die Aufrufparameter zu aktivieren, aber meiner Meinung nach ist es ziemlich dasselbe. Wie würden Sie vorschlagen, das Aufrufen externer APIs neu zu gestalten? Ich stimme zu, dass das Verspotten auf ein Minimum reduziert werden sollte. Ich sage nur, dass Sie die Daten, die Sie an externe Dienste senden, nicht testen können, um sicherzustellen, dass sich die Logik wie erwartet verhält.
Daniel Dubovski


17

Ich muss das immer wieder nachschlagen, also hier ist meine Antwort.


Das Aktivieren mehrerer Methodenaufrufe für verschiedene Objekte derselben Klasse

Angenommen, wir haben eine Hochleistungsklasse (die wir verspotten wollen):

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))
   ...:  

Hier ist ein Code, der zwei Instanzen der HeavyDutyKlasse verwendet:

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


Hier ist ein Testfall für die heavy_workFunktion:

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)
   ...:  

Wir verspotten die HeavyDutyKlasse mit MockHeavyDuty. Um Methodenaufrufe zu bestätigen, die von jeder HeavyDutyInstanz kommen, müssen wir MockHeavyDuty.return_value.assert_has_callsstattdessen auf verweisen MockHeavyDuty.assert_has_calls. Außerdem müssen expected_callswir in der Liste von angeben, für welchen Methodennamen wir Aufrufe geltend machen möchten. Unsere Liste besteht also nicht nur aus Anrufen call.do_work, sondern aus Anrufencall .

Das Ausüben des Testfalls zeigt uns, dass es erfolgreich ist:

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


Wenn wir die heavy_workFunktion ändern , schlägt der Test fehl und es wird eine hilfreiche Fehlermeldung ausgegeben:

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


Aktivieren mehrerer Aufrufe einer Funktion

Im Gegensatz dazu sehen Sie hier ein Beispiel, das zeigt, wie mehrere Aufrufe einer Funktion verspottet werden:

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


Es gibt zwei Hauptunterschiede. Die erste ist, dass wir beim Verspotten einer Funktion unsere erwarteten Aufrufe mit callanstatt mit verwenden call.some_method. Das zweite ist , dass wir rufen Sie assert_has_callsauf mock_work_function, anstatt auf mock_work_function.return_value.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.