Es ist nur eine Tatsache des Lebens, da Code wächst Schließlich müssen Sie anfangen, Mocks zu Ihrer Testsuite hinzuzufügen. Was als süßes kleines Zwei-Klassen-Projekt begann, spricht jetzt mit externen Diensten und Sie können es nicht mehr bequem testen.
Deshalb wird Python mit unittest.mock
ausgeliefert, einem leistungsstarken Teil der Standardbibliothek zum Stoppen von Abhängigkeiten und Verspotten von Nebenwirkungen.
unittest.mock
ist jedoch nicht besonders intuitiv.,
Ich habe mich oft gefragt, warum mein Go-to-Rezept für einen bestimmten Fall nicht funktioniert, also habe ich dieses Cheatsheet zusammengestellt, um mir und anderen zu helfen, schnell zu arbeiten.
Die Codebeispiele finden Sie im Github-Repository des Artikels. Ich werde Python 3.6 verwenden, wenn Sie 3.2 oder darunter verwenden, müssen Sie das mock
PyPI Paket verwenden.,
Die Beispiele werden mit unittest.TestCase
– Klassen geschrieben, um sie der Einfachheit halber ohne Abhängigkeiten auszuführen, aber Sie könnten sie als Funktionen mit pytest
fast direkt schreiben,unittest.mock
funktioniert einwandfrei. Wenn Sie ein pytest
– Benutzer sind, empfehle ich Ihnen jedoch, sich die hervorragende pytest-mock
– Bibliothek anzusehen.
Der Mittelpunkt desunittest.mock
Moduls ist natürlich dieMock
Klasse., Das Hauptmerkmal eines Mock
– Objekts ist, dass es eine andere Mock
– Instanz zurückgibt, wenn:
- Zugriff auf eines seiner Attribute
- Aufruf des Objekts selbst
/div>
Dies ist das Standardverhalten, kann jedoch auf verschiedene Arten überschrieben werden. Zum Beispiel können Sie einem Attribut in der Mock
einen Wert zuweisen, indem Sie:
- ihn direkt zuweisen, wie Sie es mit jedem Python-Objekt tun würden.,
- Verwenden Sie die
configure_mock
– Methode für eine Instanz. - Oder übergeben Sie Schlüsselwortargumente bei der Erstellung an die Klasse
Mock
.
Um Aufrufe des Scheins zu überschreiben, müssen Sie die Eigenschaft return_value
konfigurieren, die auch als Schlüsselwortargument im Initialisierer Mock
verfügbar ist., Mock
immer wieder den gleichen Wert auf alle Anrufe aus, auch in diesem Fall kann auch konfiguriert werden, indem die side_effect
attribute:
- wenn Sie möchten, um unterschiedliche Werte zurückgeben, auf jeden Anruf, den Sie zuweisen können, eine durchsuchbar zu
side_effect
. - Wenn Sie beim Aufruf des Scheins eine Ausnahme auslösen möchten, können Sie das Ausnahmeobjekt einfach
side_effect
zuweisen.,
Mit all diesen Tools können wir jetzt Stubs für im Wesentlichen jedes Python-Objekt erstellen, das hervorragend für Eingaben in unser System geeignet ist. Aber was ist mit Ausgaben?
Wenn Sie ein CLI-Programm erstellen, um das gesamte Internet herunterzuladen, möchten Sie wahrscheinlich nicht bei jedem Test das gesamte Internet herunterladen. Stattdessen würde es ausreichen zu behaupten, dass requests.download_internet
(keine echte Methode) entsprechend aufgerufen wurde. Mock
gibt Ihnen praktische Methoden, dies zu tun.,
Hinweis: in unserem Beispiel assert_called_once
fehlgeschlagen, dieser präsentiert einen weiteren wichtigen Aspekt des Mock
– Objekte, die Sie aufzeichnen, werden alle Interaktionen mit Ihnen und du kannst dann untersuchen diese Interaktionen.
Zum Beispiel können Sie call_count
verwenden, um die Anzahl der Aufrufe an die Mock
abzurufen, und call_args
oder call_args_list
verwenden, um die Argumente für die letzten bzw. alle Aufrufe zu überprüfen.,
Wenn dies zu irgendeinem Zeitpunkt unbequem ist, können Sie die Methode reset_mock
verwenden, um die aufgezeichneten Interaktionen zu löschen, beachten Sie, dass die Konfiguration nicht zurückgesetzt wird, nur die Interaktionen.
Abschließend möchte ich MagicMock
vorstellen, eine Unterklasse von Mock
, die Standard-Magic-oder Dunder-Methoden implementiert. Dies macht MagicMock
ideal, um das Klassenverhalten zu verspotten, weshalb es beim Patchen die Standardklasse ist.,
Wir sind jetzt bereit, das zu testende Gerät zu verspotten und zu isolieren. Hier sind einige Rezepte zu beachten:
Patch beim Import
Die Hauptmethode zur Verwendung von unittest.mock
besteht darin, Importe im zu testenden Modul mit der Funktion patch
zu patchen.
patch
fängt import
Anweisungen ab, die durch eine Zeichenfolge identifiziert werden (dazu später mehr), und gibt eine Mock
Instanz zurück, die Sie mit den oben beschriebenen Techniken vorkonfigurieren können.,
Stellen Sie sich vor, wir möchten diese sehr einfache Funktion testen:
Hinweis Wir importieren os
und rufen getcwd
auf, um das aktuelle Arbeitsverzeichnis abzurufen. Wir möchten es jedoch nicht in unseren Tests aufrufen, da es für unseren Code nicht wichtig ist und der Rückgabewert zwischen den Umgebungen unterschiedlich sein kann, in denen er ausgeführt wird.
Wie oben erwähnt, müssen wir patch
mit einer Zeichenfolge versehen, die unseren spezifischen Import darstellt., Wir möchten nicht einfach os.getcwd
da dies es für alle Module patchen würde, stattdessen möchten wir das Modul unter Test import
von os
, dh work.os
. Wenn das Modul importiert wird, wirkt patch
magisch und gibt stattdessen eine Mock
zurück.,
Alternativ können wir die Decorator-Version von patch
verwenden, beachten Sie , dass der Test diesmal einen zusätzlichen Parameter hat: mocked_os
zu dem die Mock
wird in den Test injiziert.,
patch
leitet Schlüsselwortargumente an die Mock
– Klasse weiter, um eine return_value
zu konfigurieren, fügen wir sie einfach als eine hinzu: p>
Spottklassen
Es ist durchaus üblich, Klassen ganz oder teilweise zu patchen., Helper
mit "db"
Worker
gibt den erwarteten Pfad zurück, der von Helper
geliefert wird, um Worker
in völliger Isolation müssen wir die gesamte Helper
Klasse:
Beachten Sie die doppelte return_value
im Beispiel würde die einfache Verwendung von MockHelper.get_path.return_value
nicht funktionieren, da im Code, den wir
get_path
für eine Instanz, nicht für die Klasse selbst.,
Die Verkettungssyntax ist etwas verwirrend, aber denken Sie daran, dass MagicMock
bei Aufrufen __init__
eine weitere MagicMock
zurückgibt. Hier konfigurieren wir alle gefälschtenHelper
Instanzen, die vonMockHelper
erstellt wurden, um das zurückzugeben, was wir bei Aufrufen vonget_path
Dies ist die einzige Methode, die uns in unserem Test am Herzen liegt.,
Class speccing
Eine Folge der Flexibilität von Mock
ist, dass Python, sobald wir eine Klasse verspottet haben, AttributeError
, da es einfach neue Instanzen von MagicMock
für im Grunde alles zurückgibt. Dies ist normalerweise eine gute Sache, kann aber zu verwirrendem Verhalten und möglicherweise Fehlern führen. Wenn Sie beispielsweise den folgenden Test schreiben:
wird stillschweigend bestanden, ohne dass der Tippfehler in assrt_called_once
vollständig fehlt .,
Wenn wir außerdem Helper.get_path
in Helper.get_folder
umbenennen würden, vergessen Sie jedoch, den Aufruf in Worker
Unsere Tests bestehen weiterhin:
Zum Glück Mock
kommt mit einem Werkzeug, um diese Fehler zu verhindern, speccing.
Einfach ausgedrückt, es konfiguriert Mocks vor, um nur auf Methoden zu reagieren, die tatsächlich in der Spezifikationsklasse vorhanden sind., Es gibt verschiedene Möglichkeiten, Spezifikationen zu definieren, aber am einfachsten ist es, einfach autospec=True
an den patch
– Aufruf zu übergeben, mit dem der Mock
so konfiguriert wird, dass er sich wie das verspottete Objekt verhält und bei Bedarf Ausnahmen für fehlende Attribute und falsche Signaturen auslöst., Zum Beispiel:
Partial class mocking
Wenn Sie weniger zum Testen in völliger Isolation neigen, können Sie eine Klasse auch teilweise mit patch.object
patchen
Hier kann patch.object
eine verspottete Version von get_path
konfigurieren, wobei der Rest des Verhaltens unberührt bleibt., Natürlich bedeutet dies, dass der Test nicht mehr das ist, was Sie strikt für einen Komponententest halten würden, aber Sie können damit einverstanden sein.
Integrierte Funktionen und Umgebungsvariablen verspotten
In den vorherigen Beispielen haben wir es vernachlässigt, einen bestimmten Aspekt unserer einfachen Klasse zu testen, den print
– Aufruf. Wenn dies im Kontext Ihrer Anwendung wichtig ist, z. B. bei einem CLI-Befehl, müssen wir Zusicherungen für diesen Aufruf vornehmen.,
print
ist natürlich eine integrierte Funktion in Python, was bedeutet, dass wir sie nicht in unser Modul importieren müssen, was dem widerspricht, was wir oben über das Patchen beim Import besprochen haben., Stellen Sie sich immer noch vor, wir hätten diese etwas kompliziertere Version unserer Funktion:
Wir können einen Test wie folgt schreiben:
/div>
Beachten Sie einige Dinge:
- Wir können
print
problemlos verspotten und behaupten, dass es einen Anruf gab, indem wir der „patch on import“ – Regel folgten. Dies war jedoch eine änderung eingeführt, in 3.,5, vorher mussten Sie zumpatch
Aufruf hinzufügen, umunittest.mock
zu signalisieren, um eineMock
zu erstellen, obwohl kein Import mit der Kennung übereinstimmt. - Wir verwenden
patch.dict
, um eine temporäre Umgebungsvariable inos.environ
, dies ist erweiterbar auf jedes andere Wörterbuch, das wir verspotten möchten., - Wir verschachteln mehrere
patch
Kontextmanager-Aufrufe, verwenden jedoch nuras
in der ersten, da es die ist, die wir verwenden müssen, umassert_called_once_with
aufzurufen .,
Wenn Sie Kontextmanager nicht verschachteln möchten, können Sie auch die patch
– Aufrufe in das Dekoratorformular schreiben:
Beachten Sie jedoch, dass die Reihenfolge der Argumente für den Test mit der Stapelreihenfolge der Dekoratoren übereinstimmt, und auch dass patch.dict
kein Argument einfügt.,dieses sehr häufige Szenario:
Sie könnten natürlich eine tatsächliche Fixture-Datei hinzufügen, aber in realen Fällen ist dies möglicherweise keine Option Stattdessen können wir die Ausgabe des Kontextmanagers als StringIO
Objekt:
Hier gibt es nichts Besonderes außer der magischen __enter__
Methode, wir müssen uns nur an die zugrunde liegende Mechanik von Kontextmanagern und einige clevere Verwendung unserer vertrauenswürdigen MagicMock
erinnern .,
Klassenattribute verspotten
Es gibt viele Möglichkeiten, dies zu erreichen, aber einige sind narrensicher als andere. Angenommen, Sie haben den folgenden Code geschrieben:
Sie können den Code auf zwei Arten testen:
- Wenn der zu testende Code über
self.ATTRIBUTE
auf das Attribut zugreift, können Sie einfach Folgendes festlegen: das Attribut direkt in der Instanz. Dies ist ziemlich sicher, da die Änderung auf diese einzelne Instanz beschränkt ist., - Alternativ können Sie das Attribut auch in der importierten Klasse im Test festlegen, bevor Sie eine Instanz erstellen. Dies ändert jedoch das Klassenattribut, das Sie in Ihren Test importiert haben, was sich auf die folgenden Tests auswirken kann, sodass Sie daran denken müssen, es zurückzusetzen.
Der Hauptnachteil dieser Nicht-Mock-Methode besteht darin, dass Ihre Tests fehlschlagen und der Fehler nicht direkt auf diese Namensfehlerübereinstimmung hinweist, wenn Sie das Attribut zu einem bestimmten Zeitpunkt umbenennen.,
Um dies zu lösen, können wir patch.object
für die importierte Klasse verwenden, die sich beschwert, wenn die Klasse nicht über das angegebene Attribut verfügt.
Hier sind einige Tests mit jeder Technik:
Spöttische Klassenhelfer
Das folgende Beispiel ist die Wurzel vieler Probleme beim Monkeypatching mit Mock. Es zeigt sich normalerweise in reiferen Codebasen, die Frameworks und Helfer bei der Klassendefinition verwenden., Stellen Sie sich zum Beispiel diesen hypothetischen Field
Klassenhelfer vor:
Sein Zweck ist es, ein Attribut in eine andere Klasse zu wickeln und zu verbessern, ein ähnliches Muster, das Sie häufig in ORMs oder Formularbibliotheken sehen. Kümmern Sie sich nicht zu sehr um die Details, beachten Sie nur, dass ein type
und ein default
Wert übergeben werden.,
Nehmen Sie nun dieses andere Beispiel des Produktionscodes:
Diese Klasse verwendet das Attribut Field
, indem sie die Klasse country
definiert, deren Typ und eine default
als erstes Element der COUNTRIES
Konstante. Die zu testende Logik ist, dass je nach Land ein Rabatt angewendet wird.,
Für die wir den folgenden Test schreiben könnten:
Aber das würde NICHT passieren.,
Lassen Sie uns durch den Test gehen:
- Zuerst patcht es die Standardländer in
pricer
um eine Liste mit einem einzigen Eintrag zu sein , - Dies sollte das
CountryPricer.country
Attribut standardmäßig zu diesem Eintrag machen, da seine Definitiondefault=COUNTRIES
, - Es instanziiert dann die
CountryPricer
und fragt nach dem ermäßigten Preis für !
Also, was ist Los?,
Die Hauptursache dafür liegt im Verhalten von Python während des Imports, das am besten in Luciano Ramalhos ausgezeichnetem fließendem Python in Kapitel 21 beschrieben wird:
Für Klassen ist die Geschichte anders: Zum Zeitpunkt des Imports führt der Interpreter den Body jeder Klasse aus, sogar den body von Klassen, die in anderen Klassen verschachtelt sind. Die Ausführung eines Klassentexts bedeutet, dass die Attribute und Methoden der Klasse definiert sind und dann das Klassenobjekt selbst erstellt wird.,
Wenden Sie dies auf unser Beispiel an:
- Die
country
AttributsField
Instanz wird erstellt, bevor der Test zum Zeitpunkt des Imports ausgeführt wird, - Python liest den Hauptteil der Klasse und übergibt die
COUNTRIES
definiert an diesem Punkt derField
Instanz, - Unser Testcode wird ausgeführt, aber es ist zu spät für uns,
COUNTRIES
zu patchen und eine ordnungsgemäße Assertion zu erhalten.,
Aus der obigen Beschreibung können Sie versuchen, den Import des Moduls zu verzögern, bis innerhalb der Tests etwas wie:
Dies wird jedoch immer noch nicht übergeben, da mock.patch
das Modul importiert und es dann monkeypatch, dies führt zu der gleichen Situation wie zuvor.,
Um dies zu umgehen, müssen wir den Status der CountryPricer
– Klasse zum Zeitpunkt des Tests annehmen und default
auf die bereits initialisierte Field
– Instanz patchen:
Diese Lösung ist nicht ideal, da sie Kenntnisse der Interna von Field
erfordert, die Sie möglicherweise nicht haben oder verwenden möchten Wenn Sie eine externe Bibliothek verwenden, funktioniert dies jedoch in diesem zugegebenermaßen vereinfachten Fall.,
Dieses Importzeitproblem ist eines der Hauptprobleme, auf die ich bei der Verwendung von unittest.mock
gestoßen bin. Sie müssen sich bei der Verwendung daran erinnern, dass zum Importzeitpunkt Code auf der obersten Ebene der Module ausgeführt wird, einschließlich Klassenkörper.
Wenn die Logik, die Sie testen, von einer dieser Logik abhängt, müssen Sie möglicherweise überdenken, wie Sie patch
entsprechend verwenden.,
Wrapping up
Diese Einführung und Cheatsheet sollten Sie mit unittest.mock
verspotten, aber ich ermutige Sie, die Dokumentation gründlich zu lesen, es gibt viele weitere interessante Tricks zu lernen.
Es ist erwähnenswert, dass es alternativen zu unittest.mock
, insbesondere Alex gaynors pretend
Bibliothek in Kombination mit pytest
’s monkeypatch
fixture. Wie Alex betont, können Sie mit diesen beiden Bibliotheken einen anderen Ansatz verfolgen, um das Verspotten strenger und dennoch vorhersehbarer zu machen., Auf jeden Fall ein Ansatz, der es wert ist, erkundet zu werden, aber außerhalb des Rahmens dieses Artikels.
unittest.mock
ist derzeit der Standard für das Verspotten in Python und Sie finden es in praktisch jeder Codebasis. Wenn Sie ein solides Verständnis davon haben, können Sie bessere Tests schneller schreiben.