Wenn Sie an einer Funktion arbeiten, die von der Zeit abhängt ... Wie organisieren Sie Unit-Tests? Wenn Ihre Unit-Testszenarien davon abhängen, wie Ihr Programm "jetzt" interpretiert, wie richten Sie sie ein?
Zweite Bearbeitung: Nach ein paar Tagen lesen Sie Ihre Erfahrungen
Ich kann sehen, dass sich die Techniken zur Bewältigung dieser Situation normalerweise um eines dieser drei Prinzipien drehen:
- Hinzufügen (Hardcode) einer Abhängigkeit: Fügen Sie eine kleine Ebene über der Zeitfunktion / dem Objekt hinzu und rufen Sie Ihre Datums- / Uhrzeitfunktion immer über diese Ebene auf. Auf diese Weise können Sie die Zeit während der Testfälle steuern.
- Verwenden Sie Mocks: Ihr Code bleibt genau gleich. In Ihren Tests ersetzen Sie das Zeitobjekt durch ein gefälschtes Zeitobjekt. Manchmal besteht die Lösung darin, das echte Zeitobjekt zu ändern, das von Ihrer Programmiersprache bereitgestellt wird.
- Abhängigkeitsinjektion verwenden: Erstellen Sie Ihren Code so, dass jede Zeitreferenz als Parameter übergeben wird. Anschließend haben Sie während der Tests die Kontrolle über die Parameter.
Sprachspezifische Techniken (oder Bibliotheken) sind sehr willkommen und werden verbessert, wenn ein illustrativer Code hinzukommt. Am interessantesten ist dann, wie ähnliche Prinzipien auf jeder Plattform angewendet werden können. Und ja ... Wenn ich es sofort in PHP anwenden kann, besser als besser;)
Beginnen wir mit einem einfachen Beispiel: einer einfachen Buchungsanwendung.
Angenommen, wir haben eine JSON-API und zwei Nachrichten: eine Anforderungsnachricht und eine Bestätigungsnachricht. Ein Standardszenario sieht folgendermaßen aus:
- Sie stellen eine Anfrage. Sie erhalten eine Antwort mit einem Token. Die Ressource, die zur Erfüllung dieser Anforderung benötigt wird, wird vom System für 5 Minuten blockiert.
- Sie bestätigen eine durch das Token gekennzeichnete Anforderung. Wenn das Token innerhalb von 5 Minuten ausgestellt wurde, wird es akzeptiert (die Ressource ist noch verfügbar). Wenn mehr als 5 Minuten vergangen sind, müssen Sie eine neue Anfrage stellen (die Ressource wurde freigegeben. Sie müssen ihre Verfügbarkeit erneut überprüfen).
Hier kommt das entsprechende Testszenario:
Ich mache eine Anfrage. Ich bestätige (sofort) mit dem Token, den ich erhalten habe. Meine Bestätigung wird akzeptiert.
Ich mache eine Anfrage. Ich warte 3 Minuten. Ich bestätige mit dem Token, den ich erhalten habe. Meine Bestätigung wird akzeptiert.
Ich mache eine Anfrage. Ich warte 6 Minuten. Ich bestätige mit dem Token, den ich erhalten habe. Meine Bestätigung wird abgelehnt.
Wie können wir diese Unit-Tests programmieren? Welche Architektur sollten wir verwenden, damit diese Funktionen testbar bleiben?
Bearbeiten Hinweis: Wenn wir eine Anfrage aufnehmen, wird die Zeit in einer Datenbank in einem Format gespeichert, das alle Informationen über Millisekunden verliert.
EXTRA - aber vielleicht etwas ausführlich: Hier kommen Details darüber, was ich bei meinen "Hausaufgaben" herausgefunden habe:
Ich habe mein Feature mit einer Abhängigkeit von einer eigenen Zeitfunktion erstellt. Meine Funktion VirtualDateTime hat eine statische Methode get_time (), die ich dort aufrufe, wo ich neues DateTime () aufgerufen habe. Mit dieser Zeitfunktion kann ich simulieren und steuern, wie spät es ist, damit ich Tests erstellen kann wie: "Jetzt auf den 21. Januar 2014, 16:15 Uhr einstellen. Eine Anfrage stellen. 3 Minuten weitermachen. Bestätigung vornehmen." Dies funktioniert gut, auf Kosten der Abhängigkeit und "nicht so hübscher Code".
Eine etwas stärker integrierte Lösung wäre, eine eigene myDateTime-Funktion zu erstellen, die DateTime um zusätzliche Funktionen für die "virtuelle Zeit" erweitert (vor allem jetzt auf das einstellen, was ich möchte). Dies macht den Code der ersten Lösung etwas eleganter (Verwendung von new myDateTime anstelle von new DateTime), ist jedoch sehr ähnlich: Ich muss mein Feature mit meiner eigenen Klasse erstellen und so eine Abhängigkeit erstellen.
Ich habe darüber nachgedacht, die DateTime-Funktion zu hacken, damit sie mit meiner Abhängigkeit funktioniert, wenn ich sie brauche. Gibt es jedoch eine einfache und übersichtliche Möglichkeit, eine Klasse durch eine andere zu ersetzen? (Ich glaube, ich habe eine Antwort darauf bekommen: Siehe Namespace unten).
In einer PHP-Umgebung, in der ich "Runkit" gelesen habe, kann ich dies möglicherweise dynamisch tun (die DateTime-Funktion hacken). Das Schöne ist, dass ich überprüfen kann, ob ich in einer Testumgebung ausgeführt werde, bevor ich etwas an DateTime ändere, und es in der Produktion unberührt lassen kann [1] . Dies klingt viel sicherer und sauberer als jeder manuelle DateTime-Hack.
Abhängigkeitsinjektion einer Taktfunktion in jeder Klasse, die Zeit verwendet [2] . Ist das nicht übertrieben? Ich sehe, dass es in einigen Umgebungen sehr nützlich wird [5] . In diesem Fall gefällt es mir allerdings nicht so gut.
Entfernen Sie die Zeitabhängigkeit in allen Funktionen [2] . Ist das immer machbar? (Weitere Beispiele finden Sie unten)
Namespaces verwenden [2] [3] ? Das sieht ganz gut aus. Ich könnte DateTime mit meiner eigenen DateTime-Funktion verspotten, die \ DateTime erweitert ... Verwenden Sie DateTime :: setNow ("2014-01-21 16:15:00") und DateTime :: wait ("+ 3 Minuten"). Dies deckt ziemlich genau das ab, was ich brauche. Was ist, wenn wir die time () -Funktion verwenden? Oder andere Zeitfunktionen? Ich muss ihre Verwendung in meinem Originalcode immer noch vermeiden. Oder ich müsste sicherstellen, dass jede PHP-Zeitfunktion, die ich in meinem Code verwende, in meinen Tests überschrieben wird ... Gibt es eine Bibliothek, die genau dies tun würde?
Ich habe nach einer Möglichkeit gesucht, die Systemzeit nur für einen Thread zu ändern. Es scheint, dass es keine gibt [4] . Das ist schade: Eine "einfache" PHP-Funktion zum "Einstellen der Zeit auf den 21. Januar 2014, 16h15 für diesen Thread" wäre eine großartige Funktion für diese Art von Tests.
Ändern Sie das Datum und die Uhrzeit des Systems für den Test mit exec (). Dies kann funktionieren, wenn Sie keine Angst haben, andere Dinge auf dem Server durcheinander zu bringen. Und Sie müssen die Systemzeit zurücksetzen, nachdem Sie Ihren Test ausgeführt haben. Es kann in einigen Situationen den Trick machen, fühlt sich aber ziemlich "hacky" an.
Das scheint mir ein sehr normales Problem zu sein. Ich vermisse jedoch immer noch eine einfache und generische Methode, um damit umzugehen. Vielleicht habe ich etwas verpasst? Bitte teilen Sie Ihre Erfahrungen!
HINWEIS: In einigen anderen Situationen haben wir möglicherweise ähnliche Testanforderungen.
Jede Funktion, die mit einer Art Timeout funktioniert (z. B. Schachspiel?)
Verarbeiten einer Warteschlange, die Ereignisse zu einem bestimmten Zeitpunkt auslöst (im obigen Szenario könnten wir so weitermachen: Einen Tag vor Beginn der Reservierung möchte ich dem Benutzer eine E-Mail mit allen Details senden. Testszenario ...)
Sie möchten eine Testumgebung mit Daten einrichten, die in der Vergangenheit gesammelt wurden - Sie möchten es so sehen, als ob es jetzt wäre. [1]
1 /programming/3271735/simulate-different-server-datetimes-in-php
2 /programming/4221480/how-to-change-current-time-for-unit-testing-date-functions-in-php
3 http://www.schmengler-se.de/de/2011/03/php-mocking-built-in-functions-like-time-in-unit-tests/
quelle
DateTime now
anstelle einer Uhr einen Wert an den Code zu übergeben.Antworten:
Ich werde Python-ähnlichen Pseudocode verwenden, der auf Ihren Testfällen basiert, um (hoffentlich) diese Antwort ein wenig zu erklären, anstatt nur zu entlarven. Kurz gesagt: Diese Antwort ist im Grunde nur ein Beispiel für eine kleine Abhängigkeitsinjektion / Abhängigkeitsinversion auf Einzelfunktionsebene , anstatt das Prinzip auf eine gesamte Klasse / ein ganzes Objekt anzuwenden.
Im Moment klingt es so, als ob Ihre Tests so strukturiert sind:
Mit Implementierungen etwas in der Art von:
Vergessen Sie stattdessen "jetzt" und teilen Sie beiden Funktionen mit, wann sie verwendet werden sollen. Wenn es die meiste Zeit "jetzt" sein wird, können Sie eines der beiden wichtigsten Dinge tun, um den aufrufenden Code einfach zu halten:
None
(null
in PHP) und auf "Jetzt" setzen, wenn kein Wert angegeben wurde.Beispielsweise könnten die beiden oben in der zweiten Version neu geschriebenen Funktionen folgendermaßen aussehen:
Auf diese Weise müssen vorhandene Teile des Codes nicht geändert werden, um "jetzt" zu übergeben, während die Teile, die Sie tatsächlich testen möchten , viel einfacher getestet werden können:
Dies schafft auch in Zukunft zusätzliche Flexibilität, sodass weniger Änderungen erforderlich sind, wenn wichtige Teile wiederverwendet werden müssen. Für zwei Beispiele, die in den Sinn kommen: Eine Warteschlange, in der Anforderungen möglicherweise zu spät erstellt werden müssen, aber dennoch die richtige Zeit verwenden, oder frühere Anforderungen im Rahmen von Datenüberprüfungen überprüft werden müssen.
Technisch gesehen könnte ein solches Vorausdenken gegen YAGNI verstoßen , aber wir bauen nicht wirklich auf diese theoretischen Fälle - diese Änderung dient nur dem einfacheren Testen, das diese theoretischen Fälle nur unterstützt.
Persönlich versuche ich, jede Art von Verspottung so weit wie möglich zu vermeiden und bevorzuge reine Funktionen wie oben. Auf diese Weise ist der getestete Code identisch mit dem Code, der außerhalb von Tests ausgeführt wird, anstatt dass wichtige Teile durch Mocks ersetzt werden.
Wir hatten ein oder zwei Regressionen, die seit einem guten Monat niemand mehr bemerkte, weil dieser bestimmte Teil des Codes in der Entwicklung nicht oft getestet wurde (wir haben nicht aktiv daran gearbeitet) und leicht defekt veröffentlicht wurde (es gab also keinen Fehler) Berichte), und die verwendeten Mocks versteckten einen Fehler in der Interaktion zwischen Hilfsfunktionen. Ein paar geringfügige Änderungen, sodass keine Verspottungen erforderlich waren und noch viel mehr leicht getestet werden konnten, einschließlich der Korrektur der Tests um diese Regression herum.
Dies ist derjenige, der um jeden Preis für Unit-Tests vermieden werden sollte. Es gibt wirklich keine Möglichkeit zu wissen, welche Arten von Inkonsistenzen oder Rennbedingungen eingeführt werden können, um zeitweise Fehler zu verursachen (für nicht verwandte Anwendungen oder Mitarbeiter derselben Entwicklungsbox), und ein fehlgeschlagener / fehlerhafter Test stellt die Uhr möglicherweise nicht richtig zurück.
quelle
Für mein Geld ist die Abhängigkeit von einer Uhrenklasse der klarste Weg, um zeitabhängige Tests durchzuführen. In PHP kann es so einfach sein wie eine Klasse mit einer einzelnen Funktion,
now
die die aktuelle Zeit mittime()
oder zurückgibtnew DateTime()
.Durch Injizieren dieser Abhängigkeit können Sie die Zeit in Ihren Tests manipulieren und die Echtzeit in der Produktion verwenden, ohne zu viel zusätzlichen Code schreiben zu müssen.
quelle
Als erstes müssen Sie die magischen Zahlen aus Ihrem Code entfernen. In diesem Fall Ihre "5 Minuten" magische Zahl. Sie müssen diese nicht nur zwangsläufig später ändern, wenn ein Kunde dies verlangt, sondern auch das Testen von Einheiten erheblich verbessern. Wer wartet 5 Minuten, bis der Test ausgeführt wird?
Zweitens, falls Sie dies noch nicht getan haben, verwenden Sie UTC für tatsächliche Zeitstempel. Dies verhindert Probleme mit der Sommerzeit und den meisten anderen Schluckaufzeiten.
Schalten Sie im Unit-Test die konfigurierte Dauer auf etwa 500 ms um und führen Sie Ihren normalen Test durch: Funktioniert dies auf eine Weise vor dem Timeout und auf eine andere Weise danach.
~ 1s ist für Komponententests immer noch ziemlich langsam, und Sie benötigen wahrscheinlich einen Schwellenwert, um die Maschinenleistung und Taktverschiebungen aufgrund von NTP (oder einer anderen Drift) zu berücksichtigen. Nach meiner Erfahrung ist dies jedoch der beste praktische Weg, um zeitbasierte Tests in den meisten Systemen durchzuführen. Einfach, schnell, zuverlässig. Die meisten anderen Ansätze (wie Sie festgestellt haben) erfordern eine Menge Arbeit und / oder sind furchtbar fehleranfällig.
quelle
Als ich TDD zum ersten Mal lernte, arbeitete ich in einer c # -Umgebung. Ich habe gelernt, dies zu tun, indem ich eine vollständige IoC-Umgebung habe, die auch eine enthält
ITimeService
, die für alle Zeitdienste verantwortlich ist und in Tests leicht verspottet werden kann.Heutzutage arbeite ich in Rubin und die Stubbing-Zeit ist viel einfacher - Sie stubben einfach die Stub-Zeit :
quelle
Verwenden Sie Microsoft Fakes. Anstatt Ihren Code an das Tool anzupassen, ändert das Tool Ihren Code zur Laufzeit und fügt anstelle der Standarduhr ein neues Zeitobjekt (das Sie in Ihrem Test definiert haben) ein.
Wenn Sie eine dynamische Sprache verwenden, ist Runkit genau das Gleiche.
Zum Beispiel mit Fakes:
Wenn die Textmethode aufgerufen wird
return DateTime.Now
, wird die Zeit, die Sie injiziert haben, und nicht die aktuelle Zeit zurückgegeben.quelle
Ich habe die Namespaces-Option in PHP untersucht. Namespaces bieten uns die Möglichkeit, der DateTime-Funktion ein Testverhalten hinzuzufügen. Somit bleiben unsere Originalobjekte unberührt.
Zuerst richten wir ein gefälschtes DateTime-Objekt in unserem Namespace ein, damit wir in unseren Tests verwalten können, welche Zeit als "jetzt" betrachtet wird.
In unseren Tests können wir diese Zeit dann über die statischen Funktionen DateTime :: set_now ($ my_time) und DateTime :: modify_now ("etwas Zeit hinzufügen") verwalten.
Zuerst kommt ein gefälschtes Buchungsobjekt . Dieses Objekt möchten wir testen - beobachten Sie die Verwendung von new DateTime () ohne Parameter an zwei Stellen. Ein kurzer Überblick:
Unser Datetime - Objekt überschreibt die Kerndatetime - Objekt sieht aus wie diese . Alle Funktionsaufrufe bleiben unverändert. Wir "haken" nur den Konstruktor, um unser Verhalten "Jetzt ist wann immer ich wähle" zu simulieren. Am wichtigsten,
Unsere Tests sind dann sehr einfach. So geht es mit PHP-Einheit (Vollversion hier ):
Diese Struktur kann überall dort problemlos reproduziert werden, wo ich "jetzt" manuell verwalten muss: Ich muss nur das neue DateTime-Objekt einschließen und seine statischen Methoden in meinen Tests verwenden.
quelle
new Datetime
der Zielklasse : Geben Sie eine separate Methode ein, um verspottbar zu sein.