unittest.mock — Erste Schritte¶
Hinzugefügt in Version 3.3.
Mocking verwenden¶
Mock-Patching-Methoden¶
Häufige Anwendungsfälle für Mock-Objekte umfassen
Methoden patchen
Aufzeichnen von Methodenaufrufen auf Objekten
Sie möchten möglicherweise eine Methode eines Objekts ersetzen, um zu überprüfen, ob sie mit den richtigen Argumenten von einem anderen Teil des Systems aufgerufen wird.
>>> real = SomeClass()
>>> real.method = MagicMock(name='method')
>>> real.method(3, 4, 5, key='value')
<MagicMock name='method()' id='...'>
Sobald unser Mock verwendet wurde (real.method in diesem Beispiel), verfügt er über Methoden und Attribute, die es Ihnen ermöglichen, Behauptungen darüber aufzustellen, wie er verwendet wurde.
Hinweis
In den meisten dieser Beispiele sind die Klassen Mock und MagicMock austauschbar. Da MagicMock die leistungsfähigere Klasse ist, ist sie eine sinnvolle Standardwahl.
Nachdem der Mock aufgerufen wurde, wird sein Attribut called auf True gesetzt. Wichtiger ist, dass wir die Methode assert_called_with() oder assert_called_once_with() verwenden können, um zu überprüfen, ob sie mit den richtigen Argumenten aufgerufen wurde.
Dieses Beispiel testet, dass das Aufrufen von ProductionClass().method zu einem Aufruf der Methode something führt.
>>> class ProductionClass:
... def method(self):
... self.something(1, 2, 3)
... def something(self, a, b, c):
... pass
...
>>> real = ProductionClass()
>>> real.something = MagicMock()
>>> real.method()
>>> real.something.assert_called_once_with(1, 2, 3)
Mock für Methodenaufrufe auf einem Objekt¶
Im letzten Beispiel haben wir eine Methode direkt auf einem Objekt gepatcht, um zu überprüfen, ob sie korrekt aufgerufen wurde. Ein weiterer häufiger Anwendungsfall ist das Übergeben eines Objekts an eine Methode (oder einen Teil des zu testenden Systems) und dann die Überprüfung, ob es auf die richtige Weise verwendet wird.
Die einfache ProductionClass unten hat eine Methode closer. Wenn sie mit einem Objekt aufgerufen wird, ruft sie close darauf auf.
>>> class ProductionClass:
... def closer(self, something):
... something.close()
...
Um sie zu testen, müssen wir also ein Objekt mit einer close-Methode übergeben und prüfen, ob es korrekt aufgerufen wurde.
>>> real = ProductionClass()
>>> mock = Mock()
>>> real.closer(mock)
>>> mock.close.assert_called_with()
Wir müssen keine Arbeit leisten, um die 'close'-Methode auf unserem Mock bereitzustellen. Der Zugriff auf 'close' erstellt sie. Wenn 'close' also noch nicht aufgerufen wurde, erstellt der Zugriff im Test sie, aber assert_called_with() löst eine Fehlermeldung aus.
Mocking von Klassen¶
Ein häufiger Anwendungsfall ist das Mocking von Klassen, die von Ihrem zu testenden Code instanziiert werden. Wenn Sie eine Klasse patchen, wird diese Klasse durch einen Mock ersetzt. Instanzen werden durch *Aufrufen der Klasse* erstellt. Das bedeutet, dass Sie auf die „Mock-Instanz“ zugreifen, indem Sie den Rückgabewert der gemockten Klasse betrachten.
Im folgenden Beispiel haben wir eine Funktion some_function, die Foo instanziiert und eine Methode darauf aufruft. Der Aufruf von patch() ersetzt die Klasse Foo durch einen Mock. Die Foo-Instanz ist das Ergebnis des Aufrufs des Mocks, daher wird sie konfiguriert, indem der return_value des Mocks geändert wird.
>>> def some_function():
... instance = module.Foo()
... return instance.method()
...
>>> with patch('module.Foo') as mock:
... instance = mock.return_value
... instance.method.return_value = 'the result'
... result = some_function()
... assert result == 'the result'
Benennen Ihrer Mocks¶
Es kann nützlich sein, Ihren Mocks einen Namen zu geben. Der Name wird in der repr-Darstellung des Mocks angezeigt und kann hilfreich sein, wenn der Mock in Fehlermeldungen von Tests erscheint. Der Name wird auch auf Attribute oder Methoden des Mocks übertragen.
>>> mock = MagicMock(name='foo')
>>> mock
<MagicMock name='foo' id='...'>
>>> mock.method
<MagicMock name='foo.method' id='...'>
Alle Aufrufe verfolgen¶
Oft möchten Sie mehr als einen einzelnen Aufruf einer Methode verfolgen. Das Attribut mock_calls zeichnet alle Aufrufe von untergeordneten Attributen des Mocks auf – und auch von deren Kindern.
>>> mock = MagicMock()
>>> mock.method()
<MagicMock name='mock.method()' id='...'>
>>> mock.attribute.method(10, x=53)
<MagicMock name='mock.attribute.method()' id='...'>
>>> mock.mock_calls
[call.method(), call.attribute.method(10, x=53)]
Wenn Sie eine Behauptung über mock_calls aufstellen und unerwartete Methoden aufgerufen wurden, schlägt die Behauptung fehl. Dies ist nützlich, da Sie nicht nur behaupten, dass die erwarteten Aufrufe ausgeführt wurden, sondern auch überprüfen, ob sie in der richtigen Reihenfolge und ohne zusätzliche Aufrufe ausgeführt wurden.
Sie verwenden das Objekt call, um Listen für den Vergleich mit mock_calls zu erstellen.
>>> expected = [call.method(), call.attribute.method(10, x=53)]
>>> mock.mock_calls == expected
True
Parameter von Aufrufen, die Mocks zurückgeben, werden jedoch nicht aufgezeichnet, was bedeutet, dass es nicht möglich ist, verschachtelte Aufrufe zu verfolgen, bei denen die zur Erstellung von Vorgängern verwendeten Parameter wichtig sind.
>>> m = Mock()
>>> m.factory(important=True).deliver()
<Mock name='mock.factory().deliver()' id='...'>
>>> m.mock_calls[-1] == call.factory(important=False).deliver()
True
Rückgabewerte und Attribute festlegen¶
Das Festlegen von Rückgabewerten für ein Mock-Objekt ist trivial einfach.
>>> mock = Mock()
>>> mock.return_value = 3
>>> mock()
3
Natürlich können Sie dasselbe für Methoden des Mocks tun.
>>> mock = Mock()
>>> mock.method.return_value = 3
>>> mock.method()
3
Der Rückgabewert kann auch im Konstruktor festgelegt werden.
>>> mock = Mock(return_value=3)
>>> mock()
3
Wenn Sie ein Attribut auf Ihrem Mock festlegen müssen, tun Sie es einfach.
>>> mock = Mock()
>>> mock.x = 3
>>> mock.x
3
Manchmal möchten Sie eine komplexere Situation simulieren, z. B. mock.connection.cursor().execute("SELECT 1"). Wenn dieser Aufruf eine Liste zurückgeben soll, müssen wir das Ergebnis des verschachtelten Aufrufs konfigurieren.
Wir können call verwenden, um die Menge der Aufrufe in einem „verketteten Aufruf“ wie diesem für einfache Behauptungen danach zu erstellen.
>>> mock = Mock()
>>> cursor = mock.connection.cursor.return_value
>>> cursor.execute.return_value = ['foo']
>>> mock.connection.cursor().execute("SELECT 1")
['foo']
>>> expected = call.connection.cursor().execute("SELECT 1").call_list()
>>> mock.mock_calls
[call.connection.cursor(), call.connection.cursor().execute('SELECT 1')]
>>> mock.mock_calls == expected
True
Es ist der Aufruf von .call_list(), der unser Aufrufobjekt in eine Liste von Aufrufen umwandelt, die die verketteten Aufrufe darstellen.
Ausnahmen mit Mocks auslösen¶
Ein nützliches Attribut ist side_effect. Wenn Sie dies auf eine Ausnahmeklasse oder -instanz setzen, wird die Ausnahme ausgelöst, wenn der Mock aufgerufen wird.
>>> mock = Mock(side_effect=Exception('Boom!'))
>>> mock()
Traceback (most recent call last):
...
Exception: Boom!
Seiten-Effekt-Funktionen und Iterables¶
side_effect kann auch auf eine Funktion oder ein Iterable gesetzt werden. Der Anwendungsfall für side_effect ist, wenn Ihr Mock mehrmals aufgerufen wird und Sie möchten, dass jeder Aufruf einen anderen Wert zurückgibt. Wenn Sie side_effect auf ein Iterable setzen, gibt jeder Aufruf des Mocks den nächsten Wert aus dem Iterable zurück.
>>> mock = MagicMock(side_effect=[4, 5, 6])
>>> mock()
4
>>> mock()
5
>>> mock()
6
Für fortgeschrittenere Anwendungsfälle, wie z. B. das dynamische Variieren der Rückgabewerte je nach Aufrufargumenten des Mocks, kann side_effect eine Funktion sein. Die Funktion wird mit denselben Argumenten wie der Mock aufgerufen. Was auch immer die Funktion zurückgibt, ist das, was der Aufruf zurückgibt.
>>> vals = {(1, 2): 1, (2, 3): 2}
>>> def side_effect(*args):
... return vals[args]
...
>>> mock = MagicMock(side_effect=side_effect)
>>> mock(1, 2)
1
>>> mock(2, 3)
2
Mocking von asynchronen Iteratoren¶
Seit Python 3.8 unterstützen AsyncMock und MagicMock das Mocking von Asynchronen Iteratoren über __aiter__. Das Attribut return_value von __aiter__ kann verwendet werden, um die für die Iteration zu verwendenden Rückgabewerte festzulegen.
>>> mock = MagicMock() # AsyncMock also works here
>>> mock.__aiter__.return_value = [1, 2, 3]
>>> async def main():
... return [i async for i in mock]
...
>>> asyncio.run(main())
[1, 2, 3]
Mocking eines asynchronen Kontextmanagers¶
Seit Python 3.8 unterstützen AsyncMock und MagicMock das Mocking von Asynchronen Kontextmanagern über __aenter__ und __aexit__. Standardmäßig sind __aenter__ und __aexit__ AsyncMock-Instanzen, die eine asynchrone Funktion zurückgeben.
>>> class AsyncContextManager:
... async def __aenter__(self):
... return self
... async def __aexit__(self, exc_type, exc, tb):
... pass
...
>>> mock_instance = MagicMock(AsyncContextManager()) # AsyncMock also works here
>>> async def main():
... async with mock_instance as result:
... pass
...
>>> asyncio.run(main())
>>> mock_instance.__aenter__.assert_awaited_once()
>>> mock_instance.__aexit__.assert_awaited_once()
Erstellen eines Mocks aus einem vorhandenen Objekt¶
Ein Problem bei übermäßigem Mocking ist, dass es Ihre Tests an die Implementierung Ihrer Mocks und nicht an Ihren echten Code koppelt. Angenommen, Sie haben eine Klasse, die some_method implementiert. In einem Test für eine andere Klasse stellen Sie einen Mock dieses Objekts bereit, der *ebenfalls* some_method bereitstellt. Wenn Sie später die erste Klasse refaktorieren, sodass sie some_method nicht mehr hat, bestehen Ihre Tests weiterhin, obwohl Ihr Code jetzt fehlerhaft ist!
Mock ermöglicht es Ihnen, ein Objekt als Spezifikation für den Mock bereitzustellen, indem Sie das spec-Schlüsselwortargument verwenden. Der Zugriff auf Methoden/Attribute des Mocks, die auf Ihrem Spezifikationsobjekt nicht vorhanden sind, löst sofort einen Attributfehler aus. Wenn Sie die Implementierung Ihrer Spezifikation ändern, beginnen Tests, die diese Klasse verwenden, sofort mit Fehlern, ohne dass Sie die Klasse in diesen Tests instanziieren müssen.
>>> mock = Mock(spec=SomeClass)
>>> mock.old_method()
Traceback (most recent call last):
...
AttributeError: Mock object has no attribute 'old_method'. Did you mean: 'class_method'?
Die Verwendung einer Spezifikation ermöglicht auch eine intelligentere Abgleichung von Aufrufen, die an den Mock gerichtet sind, unabhängig davon, ob einige Parameter als Positions- oder benannte Argumente übergeben wurden.
>>> def f(a, b, c): pass
...
>>> mock = Mock(spec=f)
>>> mock(1, 2, 3)
<Mock name='mock()' id='140161580456576'>
>>> mock.assert_called_with(a=1, b=2, c=3)
Wenn Sie möchten, dass dieser intelligentere Abgleich auch mit Methodenaufrufen des Mocks funktioniert, können Sie Auto-Speccing verwenden.
Wenn Sie eine stärkere Form der Spezifikation wünschen, die das Festlegen beliebiger Attribute sowie deren Abrufen verhindert, können Sie spec_set anstelle von spec verwenden.
Verwendung von side_effect zum Zurückgeben von Dateiinhalten¶
mock_open() wird verwendet, um die open()-Methode zu patchen. side_effect kann verwendet werden, um pro Aufruf ein neues Mock-Objekt zurückzugeben. Dies kann verwendet werden, um unterschiedliche Inhalte pro Datei zurückzugeben, die in einem Wörterbuch gespeichert sind.
DEFAULT = "default"
data_dict = {"file1": "data1",
"file2": "data2"}
def open_side_effect(name):
return mock_open(read_data=data_dict.get(name, DEFAULT))()
with patch("builtins.open", side_effect=open_side_effect):
with open("file1") as file1:
assert file1.read() == "data1"
with open("file2") as file2:
assert file2.read() == "data2"
with open("file3") as file2:
assert file2.read() == "default"
Patch-Dekoratoren¶
Hinweis
Bei patch() ist es wichtig, dass Sie Objekte im Namensraum patchen, in dem sie nachgeschlagen werden. Dies ist normalerweise unkompliziert, für eine schnelle Anleitung lesen Sie wo zu patchen.
Ein häufiger Bedarf in Tests ist das Patchen eines Klassenattributs oder eines Modulattributs, z. B. das Patchen eines Built-ins oder das Patchen einer Klasse in einem Modul, um zu testen, dass sie instanziiert wird. Module und Klassen sind im Wesentlichen global, daher muss das Patchen von ihnen nach dem Test rückgängig gemacht werden, sonst bleibt der Patch in anderen Tests bestehen und verursacht schwer diagnostizierbare Probleme.
Mock bietet drei praktische Dekoratoren dafür: patch(), patch.object() und patch.dict(). patch nimmt einen einzelnen String im Format package.module.Class.attribute entgegen, um das zu patchende Attribut anzugeben. Es nimmt optional auch einen Wert entgegen, mit dem das Attribut (oder die Klasse oder was auch immer) ersetzt werden soll. 'patch.object' nimmt ein Objekt und den Namen des zu patchenden Attributs entgegen, plus optional den Wert, mit dem es gepatcht werden soll.
patch.object:
>>> original = SomeClass.attribute
>>> @patch.object(SomeClass, 'attribute', sentinel.attribute)
... def test():
... assert SomeClass.attribute == sentinel.attribute
...
>>> test()
>>> assert SomeClass.attribute == original
>>> @patch('package.module.attribute', sentinel.attribute)
... def test():
... from package.module import attribute
... assert attribute is sentinel.attribute
...
>>> test()
Wenn Sie ein Modul patchen (einschließlich builtins), verwenden Sie patch() anstelle von patch.object().
>>> mock = MagicMock(return_value=sentinel.file_handle)
>>> with patch('builtins.open', mock):
... handle = open('filename', 'r')
...
>>> mock.assert_called_with('filename', 'r')
>>> assert handle == sentinel.file_handle, "incorrect file handle returned"
Der Modulname kann „punktiert“ sein, im Format package.module, falls erforderlich.
>>> @patch('package.module.ClassName.attribute', sentinel.attribute)
... def test():
... from package.module import ClassName
... assert ClassName.attribute == sentinel.attribute
...
>>> test()
Ein schönes Muster ist es, Testmethoden selbst zu dekorieren.
>>> class MyTest(unittest.TestCase):
... @patch.object(SomeClass, 'attribute', sentinel.attribute)
... def test_something(self):
... self.assertEqual(SomeClass.attribute, sentinel.attribute)
...
>>> original = SomeClass.attribute
>>> MyTest('test_something').test_something()
>>> assert SomeClass.attribute == original
Wenn Sie mit einem Mock patchen möchten, können Sie patch() mit nur einem Argument (oder patch.object() mit zwei Argumenten) verwenden. Der Mock wird für Sie erstellt und in die Testfunktion/-methode übergeben.
>>> class MyTest(unittest.TestCase):
... @patch.object(SomeClass, 'static_method')
... def test_something(self, mock_method):
... SomeClass.static_method()
... mock_method.assert_called_with()
...
>>> MyTest('test_something').test_something()
Sie können mehrere Patch-Dekoratoren nach diesem Muster stapeln.
>>> class MyTest(unittest.TestCase):
... @patch('package.module.ClassName1')
... @patch('package.module.ClassName2')
... def test_something(self, MockClass2, MockClass1):
... self.assertIs(package.module.ClassName1, MockClass1)
... self.assertIs(package.module.ClassName2, MockClass2)
...
>>> MyTest('test_something').test_something()
Wenn Sie Patch-Dekoratoren verschachteln, werden die Mocks in derselben Reihenfolge an die dekorierte Funktion übergeben, in der sie angewendet wurden (die normale *Python*-Reihenfolge der Dekoratorenanwendung). Das heißt von unten nach oben, so dass im obigen Beispiel der Mock für test_module.ClassName2 zuerst übergeben wird.
Es gibt auch patch.dict() zum Festlegen von Werten in einem Wörterbuch nur für einen bestimmten Gültigkeitsbereich und zum Wiederherstellen des ursprünglichen Zustands des Wörterbuchs, wenn der Test endet.
>>> foo = {'key': 'value'}
>>> original = foo.copy()
>>> with patch.dict(foo, {'newkey': 'newvalue'}, clear=True):
... assert foo == {'newkey': 'newvalue'}
...
>>> assert foo == original
patch, patch.object und patch.dict können alle als Kontextmanager verwendet werden.
Wenn Sie patch() verwenden, um einen Mock für Sie zu erstellen, können Sie mit der „as“-Form der with-Anweisung auf den Mock zugreifen.
>>> class ProductionClass:
... def method(self):
... pass
...
>>> with patch.object(ProductionClass, 'method') as mock_method:
... mock_method.return_value = None
... real = ProductionClass()
... real.method(1, 2, 3)
...
>>> mock_method.assert_called_with(1, 2, 3)
Alternativ können patch, patch.object und patch.dict als Klassen-Dekoratoren verwendet werden. Wenn sie so verwendet werden, ist dies dasselbe, als würde der Dekorator individuell auf jede Methode angewendet, deren Name mit „test“ beginnt.
Weitere Beispiele¶
Hier sind einige weitere Beispiele für etwas fortgeschrittenere Szenarien.
Mocking verketteter Aufrufe¶
Das Mocking von verketteten Aufrufen ist mit Mock tatsächlich unkompliziert, sobald Sie das Attribut return_value verstanden haben. Wenn ein Mock zum ersten Mal aufgerufen wird oder Sie seinen return_value abrufen, bevor er aufgerufen wurde, wird ein neuer Mock erstellt.
Das bedeutet, dass Sie sehen können, wie das Objekt, das aus einem Aufruf eines gemockten Objekts zurückgegeben wurde, verwendet wurde, indem Sie den return_value-Mock abfragen.
>>> mock = Mock()
>>> mock().foo(a=2, b=3)
<Mock name='mock().foo()' id='...'>
>>> mock.return_value.foo.assert_called_with(a=2, b=3)
Von hier aus ist es ein einfacher Schritt, verkettete Aufrufe zu konfigurieren und dann Behauptungen darüber aufzustellen. Natürlich ist eine weitere Alternative, Ihren Code von vornherein testfreundlicher zu schreiben…
Nehmen wir an, wir haben etwas Code, der ungefähr so aussieht:
>>> class Something:
... def __init__(self):
... self.backend = BackendProvider()
... def method(self):
... response = self.backend.get_endpoint('foobar').create_call('spam', 'eggs').start_call()
... # more code
Unter der Annahme, dass BackendProvider bereits gut getestet ist, wie testen wir method()? Insbesondere möchten wir testen, ob der Codeabschnitt # more code das Antwortobjekt korrekt verwendet.
Da diese Aufrufkette von einem Instanzattribut aus erfolgt, können wir das Attribut backend einer Something-Instanz per Monkey-Patching ändern. In diesem speziellen Fall interessieren wir uns nur für den Rückgabewert des letzten Aufrufs von start_call, daher müssen wir nicht viel konfigurieren. Nehmen wir an, das Objekt, das er zurückgibt, ist „dateiähnlich“, also stellen wir sicher, dass unser Antwortobjekt das eingebaute open() als seine spec verwendet.
Um dies zu tun, erstellen wir eine Mock-Instanz als unseren Mock-Backend und erstellen ein Mock-Antwortobjekt dafür. Um die Antwort als Rückgabewert für den letzten Aufruf von start_call festzulegen, könnten wir dies tun:
mock_backend.get_endpoint.return_value.create_call.return_value.start_call.return_value = mock_response
Das können wir auf etwas schönere Weise mit der Methode configure_mock() tun, um den Rückgabewert direkt für uns festzulegen.
>>> something = Something()
>>> mock_response = Mock(spec=open)
>>> mock_backend = Mock()
>>> config = {'get_endpoint.return_value.create_call.return_value.start_call.return_value': mock_response}
>>> mock_backend.configure_mock(**config)
Damit patchen wir den „Mock-Backend“ an Ort und Stelle und können den tatsächlichen Aufruf durchführen.
>>> something.backend = mock_backend
>>> something.method()
Mit mock_calls können wir den verketteten Aufruf mit einer einzigen Behauptung überprüfen. Ein verketteter Aufruf sind mehrere Aufrufe in einer Codezeile, daher gibt es mehrere Einträge in mock_calls. Wir können call.call_list() verwenden, um diese Liste von Aufrufen für uns zu erstellen.
>>> chained = call.get_endpoint('foobar').create_call('spam', 'eggs').start_call()
>>> call_list = chained.call_list()
>>> assert mock_backend.mock_calls == call_list
Partielles Mocking¶
In einigen Tests wollte ich einen Aufruf von datetime.date.today() mocken, um ein bekanntes Datum zurückzugeben, aber ich wollte nicht verhindern, dass der zu testende Code neue Datumsobjekte erstellt. Leider ist datetime.date in C geschrieben, und so konnte ich die statische Methode datetime.date.today() nicht einfach per Monkey-Patching aushebeln.
Ich fand eine einfache Möglichkeit, dies zu tun, die im Wesentlichen darin bestand, die Datumsklasse mit einem Mock zu umwickeln, aber Aufrufe an den Konstruktor an die echte Klasse weiterzuleiten (und echte Instanzen zurückzugeben).
Der patch decorator wird hier verwendet, um die date-Klasse im zu testenden Modul zu mocken. Das Attribut side_effect der Mock-Datumsklasse wird dann auf eine Lambda-Funktion gesetzt, die ein echtes Datum zurückgibt. Wenn die Mock-Datumsklasse aufgerufen wird, wird ein echtes Datum konstruiert und von side_effect zurückgegeben.
>>> from datetime import date
>>> with patch('mymodule.date') as mock_date:
... mock_date.today.return_value = date(2010, 10, 8)
... mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
...
... assert mymodule.date.today() == date(2010, 10, 8)
... assert mymodule.date(2009, 6, 8) == date(2009, 6, 8)
Beachten Sie, dass wir datetime.date nicht global patchen, sondern date in dem Modul patchen, das es *verwendet*. Siehe wo zu patchen.
Wenn date.today() aufgerufen wird, wird ein bekanntes Datum zurückgegeben, aber Aufrufe des date(...)-Konstruktors geben immer noch normale Daten zurück. Ohne dies müssen Sie möglicherweise ein erwartetes Ergebnis mit genau demselben Algorithmus wie der zu testende Code berechnen, was ein klassisches Anti-Muster beim Testen ist.
Aufrufe des Datumskonstruktors werden in den Attributen des mock_date (call_count und ähnliche) aufgezeichnet, was auch für Ihre Tests nützlich sein kann.
Eine alternative Methode zum Mocking von Daten oder anderen eingebauten Klassen wird in diesem Blogbeitrag diskutiert.
Mocking einer Generator-Methode¶
Ein Python-Generator ist eine Funktion oder Methode, die die yield-Anweisung verwendet, um eine Reihe von Werten zurückzugeben, wenn sie iteriert wird [1].
Eine Generator-Methode/-Funktion wird aufgerufen, um das Generatorobjekt zurückzugeben. Es ist das Generatorobjekt, das dann iteriert wird. Die Protokollmethode für die Iteration ist __iter__(), daher können wir dies mit einem MagicMock mocken.
Hier ist eine Beispielklasse mit einer Methode „iter“, die als Generator implementiert ist.
>>> class Foo:
... def iter(self):
... for i in [1, 2, 3]:
... yield i
...
>>> foo = Foo()
>>> list(foo.iter())
[1, 2, 3]
Wie würden wir diese Klasse und insbesondere ihre „iter“-Methode mocken?
Um die von der Iteration zurückgegebenen Werte zu konfigurieren (implizit im Aufruf von list), müssen wir das Objekt konfigurieren, das vom Aufruf von foo.iter() zurückgegeben wird.
>>> mock_foo = MagicMock()
>>> mock_foo.iter.return_value = iter([1, 2, 3])
>>> list(mock_foo.iter())
[1, 2, 3]
Den gleichen Patch auf jede Testmethode anwenden¶
Wenn Sie mehrere Patches für mehrere Testmethoden zur Verfügung haben möchten, ist der offensichtliche Weg, die Patch-Dekoratoren auf jede Methode anzuwenden. Dies kann sich wie unnötige Wiederholung anfühlen. Stattdessen können Sie patch() (in all seinen verschiedenen Formen) als Klassen-Dekorator verwenden. Dies wendet die Patches auf alle Testmethoden der Klasse an. Eine Testmethode wird durch Methoden identifiziert, deren Namen mit test beginnen.
>>> @patch('mymodule.SomeClass')
... class MyTest(unittest.TestCase):
...
... def test_one(self, MockSomeClass):
... self.assertIs(mymodule.SomeClass, MockSomeClass)
...
... def test_two(self, MockSomeClass):
... self.assertIs(mymodule.SomeClass, MockSomeClass)
...
... def not_a_test(self):
... return 'something'
...
>>> MyTest('test_one').test_one()
>>> MyTest('test_two').test_two()
>>> MyTest('test_two').not_a_test()
'something'
Eine alternative Methode zur Verwaltung von Patches ist die Verwendung der Patch-Methoden: start und stop. Diese ermöglichen es Ihnen, das Patchen in Ihre setUp und tearDown Methoden zu verlagern.
>>> class MyTest(unittest.TestCase):
... def setUp(self):
... self.patcher = patch('mymodule.foo')
... self.mock_foo = self.patcher.start()
...
... def test_foo(self):
... self.assertIs(mymodule.foo, self.mock_foo)
...
... def tearDown(self):
... self.patcher.stop()
...
>>> MyTest('test_foo').run()
Wenn Sie diese Technik verwenden, müssen Sie sicherstellen, dass das Patchen durch Aufrufen von stop „rückgängig gemacht“ wird. Dies kann kniffliger sein, als Sie denken, denn wenn eine Ausnahme in setUp ausgelöst wird, wird tearDown nicht aufgerufen. unittest.TestCase.addCleanup() erleichtert dies.
>>> class MyTest(unittest.TestCase):
... def setUp(self):
... patcher = patch('mymodule.foo')
... self.addCleanup(patcher.stop)
... self.mock_foo = patcher.start()
...
... def test_foo(self):
... self.assertIs(mymodule.foo, self.mock_foo)
...
>>> MyTest('test_foo').run()
Mocking von ungebundenen Methoden¶
Beim Schreiben von Tests musste ich heute eine *ungebundene Methode* patchen (die Methode auf der Klasse anstatt auf der Instanz patchen). Ich brauchte self als erstes Argument, weil ich Behauptungen darüber aufstellen wollte, welche Objekte diese spezielle Methode aufrufen. Das Problem ist, dass man dafür nicht mit einem Mock patchen kann, denn wenn man eine ungebundene Methode durch einen Mock ersetzt, wird sie beim Abrufen von der Instanz nicht zu einer gebundenen Methode und erhält daher self nicht als erstes Argument übergeben. Die Umgehung besteht darin, die ungebundene Methode durch eine echte Funktion zu patchen. Der patch()-Dekorator macht es so einfach, Methoden mit einem Mock auszuhebeln, dass das Erstellen einer echten Funktion zu einer Belästigung wird.
Wenn Sie autospec=True an patch übergeben, führt es das Patchen mit einem *echten* Funktionsobjekt durch. Dieses Funktionsobjekt hat die gleiche Signatur wie das, das es ersetzt, delegiert aber intern an einen Mock. Sie erhalten Ihren Mock immer noch auf genau die gleiche Weise wie zuvor automatisch erstellt. Was es bedeutet, ist jedoch, dass, wenn Sie es zum Patchen einer ungebundenen Methode einer Klasse verwenden, die gemockte Funktion in eine gebundene Methode umgewandelt wird, wenn sie von einer Instanz abgerufen wird. Sie erhält self als erstes Argument übergeben, was genau das ist, was ich wollte.
>>> class Foo:
... def foo(self):
... pass
...
>>> with patch.object(Foo, 'foo', autospec=True) as mock_foo:
... mock_foo.return_value = 'foo'
... foo = Foo()
... foo.foo()
...
'foo'
>>> mock_foo.assert_called_once_with(foo)
Wenn wir autospec=True nicht verwenden, wird die ungebundene Methode stattdessen mit einer Mock-Instanz gepatcht und nicht mit self aufgerufen.
Mehrere Aufrufe mit Mock überprüfen¶
Mock hat eine schöne API, um Aussagen darüber zu treffen, wie Ihre Mock-Objekte verwendet werden.
>>> mock = Mock()
>>> mock.foo_bar.return_value = None
>>> mock.foo_bar('baz', spam='eggs')
>>> mock.foo_bar.assert_called_with('baz', spam='eggs')
Wenn Ihr Mock nur einmal aufgerufen wird, können Sie die Methode assert_called_once_with() verwenden, die auch besagt, dass call_count eins ist.
>>> mock.foo_bar.assert_called_once_with('baz', spam='eggs')
>>> mock.foo_bar()
>>> mock.foo_bar.assert_called_once_with('baz', spam='eggs')
Traceback (most recent call last):
...
AssertionError: Expected 'foo_bar' to be called once. Called 2 times.
Calls: [call('baz', spam='eggs'), call()].
Sowohl assert_called_with als auch assert_called_once_with treffen Aussagen über den *aktuellsten* Aufruf. Wenn Ihr Mock mehrmals aufgerufen wird und Sie Aussagen über *alle* diese Aufrufe treffen möchten, können Sie call_args_list verwenden.
>>> mock = Mock(return_value=None)
>>> mock(1, 2, 3)
>>> mock(4, 5, 6)
>>> mock()
>>> mock.call_args_list
[call(1, 2, 3), call(4, 5, 6), call()]
Der Helfer call erleichtert das Treffen von Aussagen über diese Aufrufe. Sie können eine Liste erwarteter Aufrufe erstellen und diese mit call_args_list vergleichen. Dies sieht dem repr-String von call_args_list bemerkenswert ähnlich.
>>> expected = [call(1, 2, 3), call(4, 5, 6), call()]
>>> mock.call_args_list == expected
True
Umgang mit veränderbaren Argumenten¶
Eine weitere seltene, aber tückische Situation ist, wenn Ihr Mock mit veränderbaren Argumenten aufgerufen wird. call_args und call_args_list speichern *Referenzen* auf die Argumente. Wenn die Argumente vom getesteten Code verändert werden, können Sie keine Aussagen mehr darüber treffen, welche Werte sie zum Zeitpunkt des Mock-Aufrufs hatten.
Hier ist ein Beispielcode, der das Problem zeigt. Stellen Sie sich die folgenden Funktionen vor, die in 'mymodule' definiert sind:
def frob(val):
pass
def grob(val):
"First frob and then clear val"
frob(val)
val.clear()
Wenn wir versuchen zu testen, ob grob frob mit dem korrekten Argument aufruft, schauen Sie, was passiert:
>>> with patch('mymodule.frob') as mock_frob:
... val = {6}
... mymodule.grob(val)
...
>>> val
set()
>>> mock_frob.assert_called_with({6})
Traceback (most recent call last):
...
AssertionError: Expected: (({6},), {})
Called with: ((set(),), {})
Eine Möglichkeit wäre, dass Mock die übergebenen Argumente kopiert. Dies könnte dann Probleme verursachen, wenn Sie Aussagen treffen, die auf Objektidentität für Gleichheit beruhen.
Hier ist eine Lösung, die die side_effect-Funktionalität verwendet. Wenn Sie eine side_effect-Funktion für einen Mock bereitstellen, wird side_effect mit denselben Argumenten wie der Mock aufgerufen. Dies gibt uns die Möglichkeit, die Argumente zu kopieren und für spätere Aussagen zu speichern. In diesem Beispiel verwende ich *einen weiteren* Mock, um die Argumente zu speichern, damit ich die Mock-Methoden für die Aussage verwenden kann. Wieder setzt eine Hilfsfunktion dies für mich.
>>> from copy import deepcopy
>>> from unittest.mock import Mock, patch, DEFAULT
>>> def copy_call_args(mock):
... new_mock = Mock()
... def side_effect(*args, **kwargs):
... args = deepcopy(args)
... kwargs = deepcopy(kwargs)
... new_mock(*args, **kwargs)
... return DEFAULT
... mock.side_effect = side_effect
... return new_mock
...
>>> with patch('mymodule.frob') as mock_frob:
... new_mock = copy_call_args(mock_frob)
... val = {6}
... mymodule.grob(val)
...
>>> new_mock.assert_called_with({6})
>>> new_mock.call_args
call({6})
copy_call_args wird mit dem Mock aufgerufen, der aufgerufen wird. Es gibt einen neuen Mock zurück, auf dem wir die Aussage treffen. Die side_effect-Funktion erstellt eine Kopie der Argumente und ruft unseren new_mock mit der Kopie auf.
Hinweis
Wenn Ihr Mock nur einmal verwendet wird, gibt es eine einfachere Möglichkeit, Argumente zum Zeitpunkt ihres Aufrufs zu überprüfen. Sie können die Überprüfung einfach in einer side_effect-Funktion durchführen.
>>> def side_effect(arg):
... assert arg == {6}
...
>>> mock = Mock(side_effect=side_effect)
>>> mock({6})
>>> mock(set())
Traceback (most recent call last):
...
AssertionError
Ein alternativer Ansatz ist die Erstellung einer Unterklasse von Mock oder MagicMock, die die Argumente kopiert (mit copy.deepcopy()). Hier ist eine Beispielimplementierung:
>>> from copy import deepcopy
>>> class CopyingMock(MagicMock):
... def __call__(self, /, *args, **kwargs):
... args = deepcopy(args)
... kwargs = deepcopy(kwargs)
... return super().__call__(*args, **kwargs)
...
>>> c = CopyingMock(return_value=None)
>>> arg = set()
>>> c(arg)
>>> arg.add(1)
>>> c.assert_called_with(set())
>>> c.assert_called_with(arg)
Traceback (most recent call last):
...
AssertionError: expected call not found.
Expected: mock({1})
Actual: mock(set())
>>> c.foo
<CopyingMock name='mock.foo' id='...'>
Wenn Sie Mock oder MagicMock unterklassifizieren, verwenden alle dynamisch erstellten Attribute und der return_value automatisch Ihre Unterklasse. Das bedeutet, dass alle Kinder eines CopyingMock ebenfalls den Typ CopyingMock haben.
Verschachtelte Patches¶
Die Verwendung von patch als Kontextmanager ist praktisch, aber wenn Sie mehrere Patches durchführen, kann dies zu verschachtelten with-Anweisungen führen, die immer weiter nach rechts einrücken.
>>> class MyTest(unittest.TestCase):
...
... def test_foo(self):
... with patch('mymodule.Foo') as mock_foo:
... with patch('mymodule.Bar') as mock_bar:
... with patch('mymodule.Spam') as mock_spam:
... assert mymodule.Foo is mock_foo
... assert mymodule.Bar is mock_bar
... assert mymodule.Spam is mock_spam
...
>>> original = mymodule.Foo
>>> MyTest('test_foo').test_foo()
>>> assert mymodule.Foo is original
Mit den cleanup-Funktionen von unittest und den patch-Methoden: start and stop können wir denselben Effekt erzielen, ohne die verschachtelte Einrückung. Eine einfache Hilfsmethode, create_patch, platziert den Patch und gibt den erstellten Mock für uns zurück.
>>> class MyTest(unittest.TestCase):
...
... def create_patch(self, name):
... patcher = patch(name)
... thing = patcher.start()
... self.addCleanup(patcher.stop)
... return thing
...
... def test_foo(self):
... mock_foo = self.create_patch('mymodule.Foo')
... mock_bar = self.create_patch('mymodule.Bar')
... mock_spam = self.create_patch('mymodule.Spam')
...
... assert mymodule.Foo is mock_foo
... assert mymodule.Bar is mock_bar
... assert mymodule.Spam is mock_spam
...
>>> original = mymodule.Foo
>>> MyTest('test_foo').run()
>>> assert mymodule.Foo is original
Mocking eines Wörterbuchs mit MagicMock¶
Sie möchten vielleicht ein Wörterbuch oder ein anderes Containerobjekt mocken, alle Zugriffe darauf aufzeichnen und es gleichzeitig wie ein Wörterbuch verhalten lassen.
Dies können wir mit MagicMock erreichen, das sich wie ein Wörterbuch verhält, und side_effect verwenden, um den Wörterbuchzugriff an ein echtes, von uns kontrolliertes zugrunde liegendes Wörterbuch weiterzuleiten.
Wenn die Methoden __getitem__() und __setitem__() unseres MagicMock aufgerufen werden (normaler Wörterbuchzugriff), dann wird side_effect mit dem Schlüssel (und im Fall von __setitem__ auch dem Wert) aufgerufen. Wir können auch steuern, was zurückgegeben wird.
Nachdem der MagicMock verwendet wurde, können wir Attribute wie call_args_list verwenden, um Aussagen darüber zu treffen, wie das Wörterbuch verwendet wurde.
>>> my_dict = {'a': 1, 'b': 2, 'c': 3}
>>> def getitem(name):
... return my_dict[name]
...
>>> def setitem(name, val):
... my_dict[name] = val
...
>>> mock = MagicMock()
>>> mock.__getitem__.side_effect = getitem
>>> mock.__setitem__.side_effect = setitem
Hinweis
Eine Alternative zur Verwendung von MagicMock ist die Verwendung von Mock und die Bereitstellung *nur* der von Ihnen speziell gewünschten Magie-Methoden.
>>> mock = Mock()
>>> mock.__getitem__ = Mock(side_effect=getitem)
>>> mock.__setitem__ = Mock(side_effect=setitem)
Eine *dritte* Option ist die Verwendung von MagicMock, wobei dict als *spec* (oder *spec_set*) Argument übergeben wird, sodass der erstellte MagicMock nur die Magie-Methoden eines Wörterbuchs zur Verfügung hat.
>>> mock = MagicMock(spec_set=dict)
>>> mock.__getitem__.side_effect = getitem
>>> mock.__setitem__.side_effect = setitem
Mit diesen Side-Effect-Funktionen verhält sich der mock wie ein normales Wörterbuch, zeichnet aber den Zugriff auf. Er löst sogar einen KeyError aus, wenn Sie versuchen, auf einen nicht existierenden Schlüssel zuzugreifen.
>>> mock['a']
1
>>> mock['c']
3
>>> mock['d']
Traceback (most recent call last):
...
KeyError: 'd'
>>> mock['b'] = 'fish'
>>> mock['d'] = 'eggs'
>>> mock['b']
'fish'
>>> mock['d']
'eggs'
Nachdem er verwendet wurde, können Sie über den Zugriff mit den normalen Mock-Methoden und -Attributen Aussagen treffen.
>>> mock.__getitem__.call_args_list
[call('a'), call('c'), call('d'), call('b'), call('d')]
>>> mock.__setitem__.call_args_list
[call('b', 'fish'), call('d', 'eggs')]
>>> my_dict
{'a': 1, 'b': 'fish', 'c': 3, 'd': 'eggs'}
Mock-Unterklassen und ihre Attribute¶
Es gibt verschiedene Gründe, warum Sie Mock unterklassifizieren möchten. Ein Grund könnte sein, Hilfsmethoden hinzuzufügen. Hier ist ein albernes Beispiel:
>>> class MyMock(MagicMock):
... def has_been_called(self):
... return self.called
...
>>> mymock = MyMock(return_value=None)
>>> mymock
<MyMock id='...'>
>>> mymock.has_been_called()
False
>>> mymock()
>>> mymock.has_been_called()
True
Das Standardverhalten für Mock-Instanzen ist, dass Attribute und der Rückgabewert-Mock vom selben Typ sind wie der Mock, auf den sie zugegriffen werden. Dies stellt sicher, dass Mock-Attribute Mocks sind und MagicMock-Attribute MagicMocks sind [2]. Wenn Sie also zur Unterklassenbildung zur Hinzufügung von Hilfsmethoden verwenden, sind diese auch auf den Attributen und dem Rückgabewert-Mock von Instanzen Ihrer Unterklasse verfügbar.
>>> mymock.foo
<MyMock name='mock.foo' id='...'>
>>> mymock.foo.has_been_called()
False
>>> mymock.foo()
<MyMock name='mock.foo()' id='...'>
>>> mymock.foo.has_been_called()
True
Manchmal ist das unbequem. Zum Beispiel unterklassifiziert ein Benutzer mock, um einen Twisted-Adapter zu erstellen. Wenn dies auch auf Attribute angewendet wird, verursacht dies tatsächlich Fehler.
Mock (in all seinen Varianten) verwendet eine Methode namens _get_child_mock, um diese "Unter-Mocks" für Attribute und Rückgabewerte zu erstellen. Sie können verhindern, dass Ihre Unterklasse für Attribute verwendet wird, indem Sie diese Methode überschreiben. Die Signatur besagt, dass sie beliebige Schlüsselwortargumente (**kwargs) entgegennimmt, die dann an den Mock-Konstruktor übergeben werden.
>>> class Subclass(MagicMock):
... def _get_child_mock(self, /, **kwargs):
... return MagicMock(**kwargs)
...
>>> mymock = Subclass()
>>> mymock.foo
<MagicMock name='mock.foo' id='...'>
>>> assert isinstance(mymock, Subclass)
>>> assert not isinstance(mymock.foo, Subclass)
>>> assert not isinstance(mymock(), Subclass)
Eine Ausnahme von dieser Regel sind die nicht aufrufbaren Mocks. Attribute verwenden die aufrufbare Variante, da sonst nicht aufrufbare Mocks keine aufrufbaren Methoden haben könnten.
Mocking von Imports mit patch.dict¶
Eine Situation, in der Mocking schwierig sein kann, ist, wenn Sie einen lokalen Import innerhalb einer Funktion haben. Diese sind schwieriger zu mocken, da sie kein Objekt aus dem Modul-Namespace verwenden, das wir patchen können.
Generell sollten lokale Imports vermieden werden. Sie werden manchmal durchgeführt, um zirkuläre Abhängigkeiten zu vermeiden, wofür es *normalerweise* einen viel besseren Weg gibt, das Problem zu lösen (den Code refaktorisieren) oder um "Upfront-Kosten" zu vermeiden, indem der Import verzögert wird. Dies kann auch auf bessere Weise gelöst werden als ein bedingungsloser lokaler Import (speichern Sie das Modul als Klassen- oder Modulattribut und führen Sie den Import nur beim ersten Gebrauch durch).
Davon abgesehen gibt es eine Möglichkeit, mock zu verwenden, um die Ergebnisse eines Imports zu beeinflussen. Importieren holt ein *Objekt* aus dem sys.modules-Dictionary. Beachten Sie, dass ein *Objekt* geholt wird, das kein Modul sein muss. Das erstmalige Importieren eines Moduls führt dazu, dass ein Modulobjekt in sys.modules abgelegt wird, sodass Sie normalerweise, wenn Sie etwas importieren, ein Modul zurückbekommen. Dies muss jedoch nicht der Fall sein.
Das bedeutet, dass Sie patch.dict() verwenden können, um *vorübergehend* einen Mock in sys.modules zu platzieren. Jeder Import, während dieser Patch aktiv ist, wird den Mock abrufen. Wenn der Patch abgeschlossen ist (die dekorierte Funktion beendet wird, der with-Statement-Block abgeschlossen ist oder patcher.stop() aufgerufen wird), wird das, was zuvor dort war, sicher wiederhergestellt.
Hier ist ein Beispiel, das das Modul 'fooble' ausmacht.
>>> import sys
>>> mock = Mock()
>>> with patch.dict('sys.modules', {'fooble': mock}):
... import fooble
... fooble.blob()
...
<Mock name='mock.blob()' id='...'>
>>> assert 'fooble' not in sys.modules
>>> mock.blob.assert_called_once_with()
Wie Sie sehen, gelingt der import fooble, aber beim Beenden ist kein 'fooble' mehr in sys.modules.
Dies funktioniert auch für die Form from module import name.
>>> mock = Mock()
>>> with patch.dict('sys.modules', {'fooble': mock}):
... from fooble import blob
... blob.blip()
...
<Mock name='mock.blob.blip()' id='...'>
>>> mock.blob.blip.assert_called_once_with()
Mit etwas mehr Aufwand können Sie auch Paketimporte mocken.
>>> mock = Mock()
>>> modules = {'package': mock, 'package.module': mock.module}
>>> with patch.dict('sys.modules', modules):
... from package.module import fooble
... fooble()
...
<Mock name='mock.module.fooble()' id='...'>
>>> mock.module.fooble.assert_called_once_with()
Verfolgen der Aufrufreihenfolge und weniger ausführliche Aufrufaussagen¶
Die Klasse Mock ermöglicht es Ihnen, die *Reihenfolge* von Methodenaufrufen auf Ihren Mock-Objekten über das Attribut method_calls zu verfolgen. Dies erlaubt es Ihnen nicht, die Reihenfolge der Aufrufe zwischen separaten Mock-Objekten zu verfolgen, aber wir können mock_calls verwenden, um denselben Effekt zu erzielen.
Da Mocks Aufrufe an Kind-Mocks in mock_calls verfolgen und der Zugriff auf ein beliebiges Attribut eines Mocks einen Kind-Mock erstellt, können wir unsere separaten Mocks aus einem Eltern-Mock erstellen. Aufrufe an diese Kind-Mocks werden dann alle, in Reihenfolge, in den mock_calls des Elternteils aufgezeichnet.
>>> manager = Mock()
>>> mock_foo = manager.foo
>>> mock_bar = manager.bar
>>> mock_foo.something()
<Mock name='mock.foo.something()' id='...'>
>>> mock_bar.other.thing()
<Mock name='mock.bar.other.thing()' id='...'>
>>> manager.mock_calls
[call.foo.something(), call.bar.other.thing()]
Wir können dann über die Aufrufe, einschließlich der Reihenfolge, Aussagen treffen, indem wir mit dem Attribut mock_calls des Manager-Mocks vergleichen.
>>> expected_calls = [call.foo.something(), call.bar.other.thing()]
>>> manager.mock_calls == expected_calls
True
Wenn patch Ihre Mocks erstellt und platziert, können Sie sie mit der Methode attach_mock() an einen Manager-Mock anhängen. Nach dem Anhängen werden Aufrufe in den mock_calls des Managers aufgezeichnet.
>>> manager = MagicMock()
>>> with patch('mymodule.Class1') as MockClass1:
... with patch('mymodule.Class2') as MockClass2:
... manager.attach_mock(MockClass1, 'MockClass1')
... manager.attach_mock(MockClass2, 'MockClass2')
... MockClass1().foo()
... MockClass2().bar()
<MagicMock name='mock.MockClass1().foo()' id='...'>
<MagicMock name='mock.MockClass2().bar()' id='...'>
>>> manager.mock_calls
[call.MockClass1(),
call.MockClass1().foo(),
call.MockClass2(),
call.MockClass2().bar()]
Wenn viele Aufrufe getätigt wurden, Sie aber nur an einer bestimmten Sequenz interessiert sind, ist eine Alternative die Verwendung der Methode assert_has_calls(). Diese nimmt eine Liste von Aufrufen (konstruiert mit dem call-Objekt) entgegen. Wenn diese Sequenz von Aufrufen in mock_calls vorhanden ist, ist die Aussage erfolgreich.
>>> m = MagicMock()
>>> m().foo().bar().baz()
<MagicMock name='mock().foo().bar().baz()' id='...'>
>>> m.one().two().three()
<MagicMock name='mock.one().two().three()' id='...'>
>>> calls = call.one().two().three().call_list()
>>> m.assert_has_calls(calls)
Obwohl die verketteten Aufrufe m.one().two().three() nicht die einzigen Aufrufe sind, die an den Mock getätigt wurden, ist die Aussage dennoch erfolgreich.
Manchmal können an einem Mock mehrere Aufrufe getätigt worden sein, und Sie sind nur daran interessiert, *einige* dieser Aufrufe zu überprüfen. Möglicherweise ist Ihnen die Reihenfolge sogar egal. In diesem Fall können Sie any_order=True an assert_has_calls übergeben.
>>> m = MagicMock()
>>> m(1), m.two(2, 3), m.seven(7), m.fifty('50')
(...)
>>> calls = [call.fifty('50'), call(1), call.seven(7)]
>>> m.assert_has_calls(calls, any_order=True)
Komplexere Argumentenübereinstimmung¶
Basierend auf dem gleichen Grundkonzept wie ANY können wir Matcher implementieren, um komplexere Aussagen über Objekte zu treffen, die als Argumente für Mocks verwendet werden.
Angenommen, wir erwarten, dass ein Objekt an einen Mock übergeben wird, das standardmäßig anhand der Objektidentität gleich verglichen wird (was die Standardeinstellung von Python für benutzerdefinierte Klassen ist). Um assert_called_with() zu verwenden, müssten wir genau dasselbe Objekt übergeben. Wenn wir uns nur für einige der Attribute dieses Objekts interessieren, können wir einen Matcher erstellen, der diese Attribute für uns prüft.
In diesem Beispiel sehen Sie, wie ein "normaler" Aufruf von assert_called_with nicht ausreicht.
>>> class Foo:
... def __init__(self, a, b):
... self.a, self.b = a, b
...
>>> mock = Mock(return_value=None)
>>> mock(Foo(1, 2))
>>> mock.assert_called_with(Foo(1, 2))
Traceback (most recent call last):
...
AssertionError: expected call not found.
Expected: mock(<__main__.Foo object at 0x...>)
Actual: mock(<__main__.Foo object at 0x...>)
Eine Vergleichsfunktion für unsere Foo-Klasse könnte etwa so aussehen:
>>> def compare(self, other):
... if not type(self) == type(other):
... return False
... if self.a != other.a:
... return False
... if self.b != other.b:
... return False
... return True
...
Und ein Matcher-Objekt, das Vergleichsfunktionen wie diese für seine Gleichheitsoperation verwenden kann, sähe etwa so aus:
>>> class Matcher:
... def __init__(self, compare, some_obj):
... self.compare = compare
... self.some_obj = some_obj
... def __eq__(self, other):
... return self.compare(self.some_obj, other)
...
Alles zusammen:
>>> match_foo = Matcher(compare, Foo(1, 2))
>>> mock.assert_called_with(match_foo)
Der Matcher wird mit unserer Vergleichsfunktion und dem Foo-Objekt instanziiert, mit dem verglichen werden soll. In assert_called_with wird die Gleichheitsmethode des Matcher aufgerufen, die das Objekt, mit dem der Mock aufgerufen wurde, mit dem vergleicht, mit dem wir unseren Matcher erstellt haben. Wenn sie übereinstimmen, ist assert_called_with erfolgreich, und wenn nicht, wird ein AssertionError ausgelöst.
>>> match_wrong = Matcher(compare, Foo(3, 4))
>>> mock.assert_called_with(match_wrong)
Traceback (most recent call last):
...
AssertionError: Expected: ((<Matcher object at 0x...>,), {})
Called with: ((<Foo object at 0x...>,), {})
Mit ein paar Anpassungen könnten Sie die Vergleichsfunktion direkt die AssertionError auslösen lassen und eine nützlichere Fehlermeldung bereitstellen.
Seit Version 1.5 bietet die Python-Testbibliothek PyHamcrest ähnliche Funktionalitäten, die hier nützlich sein könnten, in Form seines Gleichheitsmatchers (hamcrest.library.integration.match_equality).