Vorgehensweise zum Ausführen von Unit-Tests in Python Ohne Testen Ihre Geduld
Mehr als oft nicht die software, die wir schreiben, direkt interagiert mit dem, was wir möchten Labels, wie „dirty“ – Dienste. Im Sinne des Laien: Dienste, die für unsere Anwendung von entscheidender Bedeutung sind, deren Wechselwirkungen aber beabsichtigte, aber unerwünschte Nebenwirkungen haben-also im Rahmen eines autonomen Testlaufs unerwünscht sind.,
Zum Beispiel: Vielleicht schreiben wir eine soziale App und möchten unsere neue „Post to Facebook-Funktion“ testen, aber nicht jedes Mal, wenn wir unsere Testsuite ausführen, tatsächlich auf Facebook posten.
Die Python—Bibliothek unittest
enthält ein Unterpaket mit dem Namen unittest.mock
—oder wenn Sie es als Abhängigkeit deklarieren, einfach mock
-das äußerst leistungsstarke und nützliche Mittel bietet, um diese unerwünschten Nebenwirkungen zu verspotten und zu beseitigen.,
Hinweis: mock
ist ab Python 3.3 neu in der Standardbibliothek enthalten; Frühere Distributionen müssen die über PyPI herunterladbare Mock-Bibliothek verwenden.
Systemaufrufe vs. Python-Spott
Um Ihnen ein weiteres Beispiel zu geben, und eines, das wir für den Rest des Artikels ausführen werden, betrachten Sie Systemaufrufe., Es ist nicht schwer zu erkennen, dass dies Hauptkandidaten für Spott sind: Egal, ob Sie ein Skript schreiben, um ein CD-Laufwerk auszuwerfen, einen Webserver, der veraltete Cache-Dateien von /tmp
entfernt, oder einen Socket-Server, der an einen TCP-Port bindet, diese Aufrufe weisen alle unerwünschte Nebenwirkungen im Kontext Ihrer Unit-Tests auf.
Als Entwickler ist es Ihnen wichtiger, dass Ihre Bibliothek die Systemfunktion zum Auswerfen einer CD erfolgreich aufgerufen hat (mit den richtigen Argumenten usw.) im Gegensatz zu tatsächlich erleben Sie Ihre CD-Tray geöffnet jedes Mal, wenn ein Test ausgeführt wird. (Oder schlimmer noch, mehrmals, da mehrere Tests während eines einzelnen Unit-Testlaufs auf den Eject-Code verweisen!)
Wenn Sie Ihre Komponententests effizient und performant halten, müssen Sie so viel „langsamen Code“ aus den automatisierten Testläufen heraushalten, nämlich Dateisystem-und Netzwerkzugriff.,
In unserem ersten Beispiel werden wir einen Standard-Python-Testfall mit mock
von der ursprünglichen Form in eine umgestalten. Wir zeigen, wie das Schreiben eines Testfalls mit Mocks unsere Tests intelligenter, schneller und in der Lage macht, mehr darüber zu verraten, wie die Software funktioniert.
Ein Einfaches Löschen-Funktion
Wir alle müssen zum löschen von Dateien aus unserem Dateisystem von Zeit zu Zeit, also schreiben wir eine Funktion in Python, die machen es ein bisschen einfacher für unsere Skripts zu tun.,
#!/usr/bin/env python# -*- coding: utf-8 -*-import osdef rm(filename): os.remove(filename)
Offensichtlich bietet unsere rm
– Methode zu diesem Zeitpunkt nicht viel mehr als die zugrunde liegende os.remove
– Methode, aber unsere Codebasis wird sich verbessern, sodass wir hier weitere Funktionen hinzufügen können.
Schreiben wir einen traditionellen Testfall, dh ohne Mocks:
Unser Testfall ist ziemlich einfach, aber jedes Mal, wenn er ausgeführt wird, wird eine temporäre Datei erstellt und dann gelöscht., Darüber hinaus können wir nicht testen, ob unsere Methode rm
das Argument ordnungsgemäß an den Aufruf os.remove
. Wir können davon ausgehen, dass dies basierend auf dem obigen Test der Fall ist, aber es bleibt viel zu wünschen übrig.
Refactoring mit Python Mocks
Lassen Sie uns unseren Testfall mit mock
:
Mit diesen Refaktoren haben wir die Funktionsweise des Tests grundlegend geändert. Jetzt haben wir einen Insider, ein Objekt, mit dem wir die Funktionalität eines anderen überprüfen können.,
Potenzielle Python – Spott-Fallstricke
Eines der ersten Dinge, die auffallen sollten, ist, dass wir den mock.patch
– Methodendekorator verwenden, um ein Objekt unter mymodule.os
zu verspotten und diesen Spott in unsere Testfallmethode einzufügen. Wäre es nicht sinnvoller, os
selbst zu verspotten, anstatt den Verweis darauf bei mymodule.os
?
Nun, Python ist eine hinterhältige Schlange, wenn es um Importe und Verwaltung von Modulen geht., Zur Laufzeit hat das mymodule
Modul eine eigene os
die in einen eigenen lokalen Bereich im Modul importiert wird. Wenn wir also os
verspotten, werden wir die Auswirkungen des Scheins im Modul mymodule
nicht sehen.
Das Mantra, das wiederholt werden soll, lautet wie folgt:
Verspotten Sie ein Element, in dem es verwendet wird, und nicht, woher es stammt.,
Wenn Sie das tempfile
Modul für myproject.app.MyElaborateClass
verspotten müssen, müssen Sie wahrscheinlich das Verspotten auf myproject.app.tempfile
anwenden, da jedes Modul seine eigenen Importe behält.
Mit dieser Falle aus dem Weg, lasst uns weiter spotten.
Validierung zu ‚rm‘ hinzufügen
Die zuvor definierte rm
– Methode ist ziemlich vereinfacht. Wir möchten, dass es überprüft, ob ein Pfad vorhanden ist und eine Datei ist, bevor wir nur blind versuchen, ihn zu entfernen., Lassen Sie uns rm
umgestalten, um ein bisschen schlauer zu sein:
#!/usr/bin/env python# -*- coding: utf-8 -*-import osimport os.pathdef rm(filename): if os.path.isfile(filename): os.remove(filename)
Großartig. Passen wir nun unseren Testfall an, um die Abdeckung aufrechtzuerhalten.
Unser Testparadigma hat sich komplett verändert. Wir können jetzt die interne Funktionalität von Methoden ohne Nebenwirkungen überprüfen und validieren.
Dateientfernung als Dienst mit Mock Patch
Bisher haben wir nur daran gearbeitet, Mocks für Funktionen bereitzustellen, jedoch nicht für Methoden für Objekte oder Fälle, in denen Mocking zum Senden von Parametern erforderlich ist. Lassen Sie uns zuerst Objektmethoden behandeln.,
Wir beginnen mit einem Refactor derrm
– Methode in eine Serviceklasse. Es ist an sich wirklich nicht gerechtfertigt, eine so einfache Funktion in ein Objekt einzukapseln, aber es wird uns zumindest helfen, Schlüsselkonzepte in mock
zu demonstrieren. Lassen Sie uns umgestalten:
Sie werden feststellen, dass sich nicht viel verändert hat in unserem test-Fall:
Große, so wir wissen jetzt, dass die RemovalService
funktioniert wie geplant., Erstellen wir einen anderen Dienst, der ihn als Abhängigkeit deklariert:
Da wir bereits eine Testabdeckung für die RemovalService
haben, werden wir die interne Funktionalität der rm
– Methode in unseren Tests von UploadService
nicht validieren. Vielmehr testen wir einfach (natürlich ohne Nebenwirkungen), dass UploadService
die RemovalService.rm
Methode RemovalService.rm
, die wir kennen „funktioniert einfach gut“ aus unserem vorherigen Testfall.
Es gibt zwei Möglichkeiten, dies zu tun:
- Verspotten Sie die
RemovalService.rm
– Methode selbst., - Geben Sie eine verspottete Instanz im Konstruktor von
UploadService
an.
Da beide Methoden beim Unit-Test oft wichtig sind, werden wir beide überprüfen.
Option 1: Instanzmethoden verspotten
Diemock
– Bibliothek verfügt über einen speziellen Methodendekorator zum Verspotten von Objektinstanzenmethoden und-eigenschaften, die @mock.patch.object
Dekorator:
Großartig! Wir haben überprüft, dass die UploadService
die rm
– Methode unserer Instanz erfolgreich aufruft. Bemerken Sie etwas Interessantes da drin?, Der Patching-Mechanismus ersetzte tatsächlich die rm
– Methode aller RemovalService
– Instanzen in unserer Testmethode. Das bedeutet, dass wir die Instanzen selbst überprüfen können. Wenn Sie mehr sehen möchten, versuchen Sie, einen Haltepunkt in Ihrem spöttischen Code einzugeben, um ein gutes Gefühl für die Funktionsweise des Patching-Mechanismus zu erhalten.
Mock Patch Pitfall: Decorator Order
Wenn Sie mehrere Dekoratoren für Ihre Testmethoden verwenden, ist die Reihenfolge wichtig und verwirrend. Wenn Sie Dekoratoren Methodenparametern zuordnen, arbeiten Sie grundsätzlich rückwärts., Betrachten Sie dieses Beispiel:
Beachten Sie, wie unsere Parameter mit der umgekehrten Reihenfolge der Dekoratoren übereinstimmen. Das liegt zum Teil an der Funktionsweise von Python. Bei mehreren Methodendekoratoren ist hier die Reihenfolge der Ausführung im Pseudocode:
patch_sys(patch_os(patch_os_path(test_something)))
Da der Patch für sys
der äußerste Patch ist, wird er zuletzt ausgeführt und ist damit der letzte Parameter in den tatsächlichen Testmethodenargumenten. Beachten Sie dies gut und verwenden Sie einen Debugger, wenn Sie Ihre Tests ausführen, um sicherzustellen, dass die richtigen Parameter in der richtigen Reihenfolge eingefügt werden.,
Option 2: Erstellen von Scheininstanzen
Anstatt die spezifische Instanzmethode zu verspotten, können wir stattdessen einfach eine verspottete Instanz an UploadService
mit seinem Konstruktor. Ich bevorzuge Option 1 oben, da es viel genauer ist, aber es gibt viele Fälle, in denen Option 2 effizient oder notwendig sein könnte. Lassen Sie uns unseren Test noch einmal umgestalten:
In diesem Beispiel mussten wir nicht einmal Funktionen patchen, wir erstellen einfach eine automatische Spezifikation für die RemovalService
Klasse und injizieren diese Instanz dann in unsere UploadService
, um die Funktionalität zu validieren.,
Diemock.create_autospec
– Methode erstellt eine funktional äquivalente Instanz zur bereitgestellten Klasse. Praktisch bedeutet dies, dass bei Interaktion mit der zurückgegebenen Instanz Ausnahmen ausgelöst werden, wenn sie auf illegale Weise verwendet werden. Genauer gesagt, wenn eine Methode mit der falschen Anzahl von Argumenten aufgerufen wird, wird eine Ausnahme ausgelöst. Dies ist äußerst wichtig, da Refaktoren passieren. Wenn sich eine Bibliothek ändert, brechen Tests und das wird erwartet. Ohne Verwendung einer automatischen Spezifikation bestehen unsere Tests weiterhin, obwohl die zugrunde liegende Implementierung fehlerhaft ist.,
Fallstrick: Die mock.Mock
und mock.MagicMock
Klassen
Die mock
Bibliothek enthält auch zwei wichtige Klassen, auf denen die meisten der internen Funktionalität aufgebaut ist: mock.Mock
und mock.MagicMock
. Wenn Sie die Wahl haben, eine mock.Mock
Instanz, eine mock.MagicMock
Instanz oder eine Auto-spec zu verwenden, bevorzugen Sie immer die Verwendung einer Auto-spec, da dies hilft, Ihre Tests für zukünftige Änderungen gesund zu halten., Dies liegt daran, dass mock.Mock
und mock.MagicMock
alle Methodenaufrufe und Eigenschaftszuweisungen unabhängig von der zugrunde liegenden API akzeptieren. Betrachten Sie den folgenden Anwendungsfall:
class Target(object): def apply(value): return valuedef method(target, value): return target.apply(value)
Wir können dies mit einer mock.Mock
-Instanz wie folgt testen:
Diese Logik scheint vernünftig zu sein, aber ändern wir die Target.apply
– Methode, um weitere Parameter zu übernehmen:
class Target(object): def apply(value, are_you_sure): if are_you_sure: return value else: return None
Führen Sie ihr Test, und Sie werden feststellen, dass es immer noch besteht. Das liegt daran, dass es nicht gegen Ihre eigentliche API aufgebaut ist., Aus diesem Grund sollten Sie immer die create_autospec
Methode und die autospec
Parameter mit dem @patch
und @patch.object
Dekoratoren.
Python-Mock-Beispiel: Verspotten eines Facebook-API-Aufrufs
Schreiben wir zum Abschluss ein anwendbareres reales Python-Mock-Beispiel, das wir in der Einleitung erwähnt haben: eine Nachricht auf Facebook posten. Wir schreiben eine nette Wrapper-Klasse und einen entsprechenden Testfall.,
Hier ist unser Testfall, der überprüft, ob wir die Nachricht veröffentlichen, ohne die Nachricht tatsächlich zu veröffentlichen:
Wie wir bisher gesehen haben, ist es wirklich einfach, intelligentere Tests mit mock
in Python zu schreiben.
Schlussfolgerung
Pythons mock
-Bibliothek ist, wenn sie etwas verwirrend ist, ein Game-Changer für Unit-Tests. Wir haben häufige Anwendungsfälle für den Einstieg mit mock
in Unit-Tests demonstriert,und hoffentlich hilft dieser Artikel Python-Entwicklern, die anfänglichen Hürden zu überwinden und exzellenten, getesteten Code zu schreiben.,