Ein Test für place_order braucht ein Payment-Gateway, einen SMS-Client und einen Steuerrechner. Setzen Sie die echten Objekte ein: Jeder Testlauf belastet dann eine Karte und verschickt eine SMS an eine echte Telefonnummer. Ein Unit-Test soll in Millisekunden laufen, ohne Netzwerkzugriff — also müssen alle drei durch etwas anderes ersetzt werden. Dieses „etwas andere“ gibt es in fünf Varianten, die jeweils ein anderes Problem lösen. Wer sie für austauschbar hält, erlebt im Code-Review immer wieder dieselbe Debatte: „dann mock das doch einfach“.
Was ist ein Test Double
Gerard Meszaros hat den Begriff „Test Double“ in xUnit Test Patterns geprägt — als Oberbegriff für alles, was bei einem Test für eine echte Abhängigkeit einspringt, so wie ein Stunt-Double für einen Schauspieler einspringt. Filmteams setzen für unterschiedliche Einstellungen unterschiedliche Doubles ein: Eines muss nur mit der richtigen Frisur im Hintergrund stehen, ein anderes übernimmt tatsächlich den Autounfall. Test Doubles teilen sich genauso auf. Dummy, Stub, Spy, Fake und Mock sind fünf unterschiedliche Aufgaben, und was sie trennt, ist das, was der Test prüft — nicht wie aufwendig das Ersatzobjekt gebaut ist.
Hier ist die Klasse, die jedes folgende Beispiel testet. Drei ihrer Abhängigkeiten lohnt es sich zu ersetzen; die vierte spielt am Ende gar keine Rolle:
| |
logger läuft nur auf dem Fehlerpfad. Das ist für das erste Beispiel wichtig.
Zwei kleine Ersatzobjekte werden in den nächsten Tests mehrfach verwendet und deshalb hier einmal definiert:
| |
Was ist ein Dummy-Objekt im Testing
Ein Dummy wird nur übergeben, um einen Parameter zu erfüllen, den der Test nie anfasst. Er tut nichts und prüft nichts. Der Happy-Path-Test unten erreicht den Exception-Zweig nie, also kann logger alles sein, was eine Zuweisung an ein Attribut überlebt:
| |
object() funktioniert, weil niemand eine Methode darauf aufruft. Würde eine spätere Änderung dazu führen, dass place_order auch auf dem Erfolgspfad loggt, schlägt dieser Test mit einem AttributeError fehl. Dieser Fehlschlag ist nützlich: Er zeigt, dass der Dummy aufgehört hat, ein Dummy zu sein.
Was ist ein Stub im Unit-Test
Ein Stub liefert eine feste Antwort, damit der Rest des Tests deterministisch bleibt. Der echte Steuerrechner ruft vielleicht eine Steuersatz-API auf. Das will der Test nicht, also liefert StubTaxCalculator immer 108.00, unabhängig vom übergebenen Betrag. Sie haben ihn bereits im Dummy-Test oben gesehen, zusammen mit StubGateway, das denselben Job für charge übernimmt.
Niemand prüft, ob total_with_tax aufgerufen wurde oder mit welchem Argument. Ein Stub liefert dem getesteten Code eine kontrollierte Eingabe; er verifiziert nie eine Interaktion. Das ist die Grenze zwischen einem Stub und den nächsten drei Doubles.
Was ist ein Test-Spy
Ein Spy zeichnet die Aufrufe auf, die er erhält, damit der Test sie nach dem Lauf des getesteten Codes untersuchen kann. unittest.mock.Mock zeichnet automatisch jeden Aufruf auf, deshalb nutzen die meisten Python-Suiten ihn als Spy, selbst wenn das Wort im Test nie auftaucht:
| |
Auf sms_spy wurde vor dem Aufruf nichts konfiguriert. Er akzeptierte, was immer er bekam, zeichnete es auf, und der Test befragte ihn danach.
Was ist ein Fake-Objekt im Testing
Ein Stub kann nicht sagen, ob zwei Belastungen hintereinander zwei unterschiedliche IDs erhalten, weil er nur einen einzigen Wert kennt. Braucht ein Test Verhalten über mehrere Aufrufe hinweg, brauchen Sie einen Fake: eine kleine Implementierung, die tatsächlich funktioniert.
| |
| |
FakeGateway hat keinen Netzwerkaufruf und keinen echten Zahlungsdienstleister dahinter, hält aber einen Zustand wie das echte Gateway. Genau diesen Zustand brauchte der Test, und ein Stub hätte ihn nicht liefern können.
Was ist ein Mock-Objekt
Nach Meszaros’ Definition trägt ein Mock seine Erwartungen in den Test hinein: Die Regeln für einen gültigen Aufruf stehen fest, bevor der Code läuft, und ein Aufruf, der sie verletzt, scheitert sofort. unittest.mock ist lockerer als das, weil Sie trotzdem erst danach prüfen — aber create_autospec bringt die wichtigere Hälfte davon. Es baut das Double aus einer echten Klasse, sodass jeder Aufruf sofort gegen die echten Methodensignaturen geprüft wird:
| |
Ändern Sie place_order so, dass es self.gateway.charge(total, currency="usd") aufruft, schlägt dieser Test mit einem TypeError innerhalb von place_order fehl, weil Gateway.charge kein currency-Argument akzeptiert. Stellen Sie sich an derselben Stelle einen einfachen Mock() vor, bei dem jemand die Assertion an den neuen Aufruf anpasst: Der Test läuft durch, und der Code bricht beim ersten echten Kontakt mit dem echten Gateway. Ein Spy bestätigt, dass ein Aufruf stattgefunden hat. Ein autospecced Mock bestätigt zusätzlich, dass es ein Aufruf ist, den das echte Objekt akzeptieren würde.
Mock vs Stub vs Spy vs Fake: Vergleichstabelle
| Double | Was es macht | Verwenden Sie es, wenn … |
|---|---|---|
| Dummy | Wird übergeben, um einen Parameter zu erfüllen; wird nie aufgerufen | Eine Abhängigkeit existiert auf diesem Pfad nur, weil die Signatur sie verlangt |
| Stub | Liefert bei jeder Eingabe dieselbe feste Antwort | Der Test braucht einen festen, deterministischen Wert aus einer Abhängigkeit |
| Spy | Zeichnet Aufrufe auf, damit der Test sie danach untersuchen kann | Der Test muss bestätigen, dass eine Interaktion stattgefunden hat, ohne sie vorher zu konfigurieren |
| Fake | Eine kleinere, aber funktionierende Implementierung des echten Dings | Der Test braucht echtes Verhalten über mehrere Aufrufe hinweg, nicht einen einzelnen Wert |
| Mock | Mit Erwartungen an gültige Aufrufe gebaut, die beim Aufruf selbst geprüft werden | Der Test muss die genaue Form eines Aufrufs prüfen: Argumente, Anzahl, Reihenfolge |
gateway war in drei Tests derselben Klasse ein Stub, ein Fake und ein Mock. Das Double richtet sich danach, was der Test prüft, nicht nach der Abhängigkeit.
Wann Test Doubles Tests schlechter machen
Jede Abhängigkeit zu mocken koppelt den Test daran, wie der Code geschrieben ist, statt daran, was er tut. Fügen Sie einen Test hinzu, der prüft, dass total_with_tax genau einmal aufgerufen wird, und refaktorieren Sie place_order so, dass es ein zweites Mal für Logging aufgerufen wird: Der Test schlägt fehl, und für die Nutzer hat sich nichts geändert. Der Fake-Test oben hat dieses Problem nicht, weil er das Ergebnis prüft (zwei unterschiedliche Charge-IDs, die richtigen erfassten Beträge) statt die Form des Aufrufs.
Eine Bibliothek zu mocken, die Sie nicht selbst besitzen, hat dasselbe Problem eine Ebene höher. Setzen Sie create_autospec direkt auf einen Drittanbieter-Client an, hängt jeder Test, der ihn berührt, von den Methodensignaturen dieser Bibliothek ab; ein kleines Update, das einen Parameter umbenennt, bricht Tests in der ganzen Suite. Verpacken Sie die Bibliothek in eine eigene kleine Schnittstelle und doppeln Sie diese stattdessen. Ändert sich die Bibliothek, ändert sich nur der Wrapper.
Manchmal ist die richtige Anzahl an Doubles null. Ist die echte Abhängigkeit bereits schnell und ohne Nebeneffekte, wie eine pure Funktion oder eine In-Memory-SQLite-Verbindung, fügt ein Double nur Indirektion hinzu und fängt nichts ab. Doubles sind für Abhängigkeiten, die langsam, entfernt, nicht-deterministisch sind oder Nebeneffekte haben, die in einem Testlauf nichts zu suchen haben.
Wie Sie das richtige Test Double wählen
Gehen Sie von dem aus, was der Test prüft, nicht von dem, was sich am sichersten anfühlt zu fälschen. Brauchen Sie eine vorhersagbare Eingabe, damit eine spätere Assertion hält, stubben Sie sie. Um zu bestätigen, dass die SMS verschickt wurde, setzen Sie einen Spy ein. Muss sich die Abhängigkeit über mehrere Aufrufe hinweg korrekt verhalten, schreiben Sie einmal einen Fake und verwenden Sie ihn überall, wo diese Abhängigkeit vorkommt. Und wenn ein Aufrufer mit falschen Argumenten den Test scheitern lassen soll, bevor er in Produktion scheitert, verwenden Sie einen autospecced Mock.
Nichts davon funktioniert bei einer Klasse, die ihre Abhängigkeiten selbst erzeugt, denn dann gibt es keine Stelle, an der Sie ein Double übergeben könnten. Dieses Problem löst Dependency Injection, und es ist das Erste, was Sie beheben sollten, wenn sich eine Klasse gegen jedes Double in diesem Artikel sperrt. Sobald die Nahtstelle existiert, entscheiden Sie, welches Double auf welcher Teststufe hingehört. Arbeiten Sie testgetrieben, greifen Sie früh zu Doubles, weil der Test existiert, bevor es echten Code zum Aufrufen gibt.