Eine Einführung in die Verspottung in Python

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, als dass Ihr CD-Fach jedes Mal geöffnet wird, wenn ein Test ausgeführt wird.,

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 mockvon 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 mockzu 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 UploadServicenicht 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:

  1. Verspotten Sie dieRemovalService.rm – Methode selbst.,
  2. Geben Sie eine verspottete Instanz im Konstruktor von UploadServicean.

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.,

Share

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.