Überschreiben Sie Java System.currentTimeMillis, um zeitkritischen Code zu testen

129

Gibt es eine Möglichkeit, entweder im Code oder mit JVM-Argumenten die aktuelle Zeit, wie sie über angezeigt wird, zu überschreiben, außer die System.currentTimeMillisSystemuhr auf dem Host-Computer manuell zu ändern?

Ein kleiner Hintergrund:

Wir haben ein System, das eine Reihe von Buchhaltungsjobs ausführt, die einen Großteil ihrer Logik um das aktuelle Datum drehen (z. B. 1. des Monats, 1. des Jahres usw.).

Leider ruft ein Großteil des Legacy-Codes Funktionen wie new Date()oder auf Calendar.getInstance(), die beide schließlich aufrufen System.currentTimeMillis.

Zu Testzwecken müssen wir derzeit die Systemuhr manuell aktualisieren, um zu ändern, zu welcher Uhrzeit und zu welchem ​​Datum der Code glaubt, dass der Test ausgeführt wird.

Meine Frage lautet also:

Gibt es eine Möglichkeit, die Rückgabe zu überschreiben System.currentTimeMillis? Um die JVM beispielsweise anzuweisen, einen Offset automatisch zu addieren oder zu subtrahieren, bevor Sie von dieser Methode zurückkehren?

Danke im Voraus!

Mike Clark
quelle
1
Ich weiß nicht mehr, ob es relevant ist, aber es gibt eine andere Methode, um dies mit AspectJ zu erreichen. Siehe meine Antwort unter: stackoverflow.com/questions/18239859/…
Nándor Előd Fekete
@ NándorElődFekete Die Lösung im Link ist interessant, erfordert jedoch eine Neukompilierung des Codes. Ich frage mich, ob das Originalplakat neu kompiliert werden kann, da er behauptet, mit Legacy-Code zu tun zu haben.
Cleberz
1
@cleberz Eine der netten Eigenschaften von AspectJ ist, dass es direkt mit Bytecode arbeitet, sodass der ursprüngliche Quellcode nicht benötigt wird, um zu funktionieren.
Nándor Előd Fekete
@ NándorElődFekete vielen Dank für den Hinweis. Ich war mir der Instrumentierung auf Bytecode-Ebene (insbesondere der Instrumentierung der JDK-Klassen) nicht bewusst. Es hat eine Weile gedauert, aber ich konnte herausfinden, dass ich sowohl das Weben der rt.jar zur Kompilierungszeit als auch das Weben der Nicht-JDK-Klassen während der Ladezeit durchführen musste, um meinen Anforderungen zu entsprechen (System überschreiben). currentTimeMillis () und System.nanoTime ()).
Cleberz
@cleberz Ich habe eine andere Antwort für das Weben durch JRE-Klassen , Sie können es auch überprüfen.
Nándor Előd Fekete

Antworten:

116

Ich empfehle dringend , dass Sie, anstatt mit der Systemuhr herumzuspielen, in die Kugel beißen und den alten Code umgestalten, um eine austauschbare Uhr zu verwenden. Idealerweise sollte dies mit der Abhängigkeitsinjektion erfolgen, aber selbst wenn Sie einen austauschbaren Singleton verwenden, erhalten Sie Testbarkeit.

Dies könnte durch Suchen und Ersetzen für die Singleton-Version fast automatisiert werden:

  • Ersetzen Calendar.getInstance()durch Clock.getInstance().getCalendarInstance().
  • Ersetzen new Date()durchClock.getInstance().newDate()
  • Ersetzen System.currentTimeMillis()durchClock.getInstance().currentTimeMillis()

(usw. nach Bedarf)

Sobald Sie diesen ersten Schritt getan haben, können Sie den Singleton nacheinander durch DI ersetzen.

Jon Skeet
quelle
49
Es ist meiner Meinung nach keine sehr gute Idee, Ihren Code durch Abstrahieren oder Umschließen aller potenziellen APIs zu überladen, um die Testbarkeit zu verbessern. Selbst wenn Sie das Refactoring einmal mit einem einfachen Suchen und Ersetzen durchführen können, ist der Code viel schwieriger zu lesen, zu verstehen und zu warten. Mehrere Mock-Frameworks sollten in der Lage sein, das Verhalten von System.currentTimeMillis zu ändern. Wenn nicht, ist die Verwendung von AOP oder selbst erstellter Instrumentalisierung die bessere Wahl.
jarnbjo
21
@jarnbjo: Nun, Sie sind natürlich zu Ihrer Meinung willkommen - aber dies ist eine ziemlich gut etablierte Technik, IMO, und ich habe sie auf jeden Fall mit großer Wirkung eingesetzt. Dies verbessert nicht nur die Testbarkeit, sondern macht auch Ihre Abhängigkeit von der Systemzeit deutlich, was hilfreich sein kann, wenn Sie sich einen Überblick über den Code verschaffen möchten.
Jon Skeet
4
@ virgo47: Ich denke, wir müssen zustimmen, nicht zuzustimmen. Ich sehe "Ihre Abhängigkeiten explizit angeben" nicht als Verschmutzung des Codes - ich sehe es als eine natürliche Möglichkeit, den Code klarer zu machen. Ich sehe es auch nicht als "vollkommen akzeptabel" an, diese Abhängigkeiten über statische Aufrufe unnötig zu verbergen.
Jon Skeet
26
UPDATE Das in Java 8 integrierte neue Paket java.time enthält eine java.time.ClockKlasse, "mit der alternative Uhren nach Bedarf angeschlossen werden können".
Basil Bourque
7
Diese Antwort impliziert, dass DI alles gut ist. Persönlich muss ich noch eine Anwendung in der realen Welt sehen, die sie gut nutzt. Stattdessen sehen wir eine Fülle sinnloser separater Schnittstellen, zustandsloser "Objekte", "Nur-Daten" -Objekte und Klassen mit geringer Kohäsion. IMO, Einfachheit und objektorientiertes Design (mit echten, zustandsbehafteten Objekten) sind die bessere Wahl. In Bezug auf staticMethoden ist sicher, dass es sich nicht um OO handelt, aber das Einfügen einer zustandslosen Abhängigkeit, deren Instanz in keinem Instanzstatus ausgeführt wird, ist nicht wirklich besser. Es ist nur eine ausgefallene Art, das ohnehin "statische" Verhalten zu verschleiern.
Rogério
77

tl; dr

Gibt es eine Möglichkeit, entweder im Code oder mit JVM-Argumenten die aktuelle Zeit zu überschreiben, wie sie über System.currentTimeMillis angezeigt wird, außer die Systemuhr auf dem Host-Computer manuell zu ändern?

Ja.

Instant.now( 
    Clock.fixed( 
        Instant.parse( "2016-01-23T12:34:56Z"), ZoneOffset.UTC
    )
)

Clock In java.time

Wir haben eine neue Lösung für das Problem des Austauschs einer steckbaren Uhr, um das Testen mit falschen Datums- / Uhrzeitwerten zu erleichtern . Das Paket java.time in Java 8 enthält eine abstrakte Klasse java.time.Clockmit einem expliziten Zweck:

damit bei Bedarf alternative Uhren angeschlossen werden können

Sie könnten Ihre eigene Implementierung von anschließen Clock, obwohl Sie wahrscheinlich eine finden können, die bereits für Ihre Anforderungen erstellt wurde. Für Ihre Bequemlichkeit enthält java.time statische Methoden, um spezielle Implementierungen zu erhalten. Diese alternativen Implementierungen können beim Testen hilfreich sein.

Veränderte Trittfrequenz

Die verschiedenen tick…Methoden erzeugen Uhren, die das aktuelle Moment mit einer anderen Trittfrequenz erhöhen.

Die Standardeinstellung gibt Clockeine Zeit an, die in Java 8 so häufig wie Millisekunden und in Java 9 so fein wie Nanosekunden aktualisiert wird (abhängig von Ihrer Hardware). Sie können verlangen, dass der wahre aktuelle Moment mit einer anderen Granularität gemeldet wird.

Falsche Uhren

Einige Uhren können lügen und ein anderes Ergebnis als die Hardware-Uhr des Host-Betriebssystems erzeugen.

  • fixed - Meldet einen einzelnen unveränderlichen (nicht inkrementierenden) Moment als aktuellen Moment.
  • offset- Meldet den aktuellen Moment, wird jedoch durch das übergebene DurationArgument verschoben .

Schließen Sie zum Beispiel den ersten Moment des frühesten Weihnachtsfestes in diesem Jahr ein. Mit anderen Worten, wenn der Weihnachtsmann und sein Rentier ihren ersten Halt machen . Die früheste Zeitzone scheint heute zu sein , Pacific/Kiritimatian +14:00.

LocalDate ld = LocalDate.now( ZoneId.of( "America/Montreal" ) );
LocalDate xmasThisYear = MonthDay.of( Month.DECEMBER , 25 ).atYear( ld.getYear() );
ZoneId earliestXmasZone = ZoneId.of( "Pacific/Kiritimati" ) ;
ZonedDateTime zdtEarliestXmasThisYear = xmasThisYear.atStartOfDay( earliestXmasZone );
Instant instantEarliestXmasThisYear = zdtEarliestXmasThisYear.toInstant();
Clock clockEarliestXmasThisYear = Clock.fixed( instantEarliestXmasThisYear , earliestXmasZone );

Verwenden Sie diese spezielle feste Uhr, um immer den gleichen Moment zurückzugeben. Wir haben den ersten Moment des Weihnachtstages in Kiritimati , wobei UTC eine Wanduhrzeit von vierzehn Stunden früher anzeigt, 10 Uhr am vorherigen Datum des 24. Dezember.

Instant instant = Instant.now( clockEarliestXmasThisYear );
ZonedDateTime zdt = ZonedDateTime.now( clockEarliestXmasThisYear );

instant.toString (): 2016-12-24T10: 00: 00Z

zdt.toString (): 2016-12-25T00: 00 + 14: 00 [Pacific / Kiritimati]

Siehe Live-Code in IdeOne.com .

Wahre Zeit, andere Zeitzone

Sie können steuern, welche Zeitzone von der ClockImplementierung zugewiesen wird . Dies kann bei einigen Tests hilfreich sein. Ich empfehle dies jedoch nicht im Produktionscode, wo Sie immer explizit die optionalen ZoneIdoder ZoneOffsetArgumente angeben sollten .

Sie können festlegen, dass UTC die Standardzone ist.

ZonedDateTime zdtClockSystemUTC = ZonedDateTime.now ( Clock.systemUTC () );

Sie können eine bestimmte Zeitzone angeben. Geben Sie einen richtigen Zeitzonennamen im Format continent/region, wie zum Beispiel America/Montreal, Africa/Casablancaoder Pacific/Auckland. Verwenden Sie niemals die Abkürzung für 3-4 Buchstaben, wie ESToder, ISTda es sich nicht um echte Zeitzonen handelt, die nicht standardisiert und nicht einmal eindeutig (!) Sind.

ZonedDateTime zdtClockSystem = ZonedDateTime.now ( Clock.system ( ZoneId.of ( "America/Montreal" ) ) );

Sie können angeben, dass die aktuelle Standardzeitzone der JVM die Standardzeitzone für ein bestimmtes ClockObjekt sein soll.

ZonedDateTime zdtClockSystemDefaultZone = ZonedDateTime.now ( Clock.systemDefaultZone () );

Führen Sie diesen Code zum Vergleichen aus. Beachten Sie, dass alle denselben Moment und denselben Punkt auf der Zeitachse melden. Sie unterscheiden sich nur in der Wanduhrzeit ; Mit anderen Worten, drei Möglichkeiten, dasselbe zu sagen, drei Möglichkeiten, denselben Moment anzuzeigen.

System.out.println ( "zdtClockSystemUTC.toString(): " + zdtClockSystemUTC );
System.out.println ( "zdtClockSystem.toString(): " + zdtClockSystem );
System.out.println ( "zdtClockSystemDefaultZone.toString(): " + zdtClockSystemDefaultZone );

America/Los_Angeles war die aktuelle JVM-Standardzone auf dem Computer, auf dem dieser Code ausgeführt wurde.

zdtClockSystemUTC.toString (): 2016-12-31T20: 52: 39.688Z

zdtClockSystem.toString (): 2016-12-31T15: 52: 39.750-05: 00 [America / Montreal]

zdtClockSystemDefaultZone.toString (): 2016-12-31T12: 52: 39.762-08: 00 [America / Los_Angeles]

Die InstantKlasse ist per Definition immer in UTC. Diese drei Clockzonenbezogenen Verwendungen haben also genau den gleichen Effekt.

Instant instantClockSystemUTC = Instant.now ( Clock.systemUTC () );
Instant instantClockSystem = Instant.now ( Clock.system ( ZoneId.of ( "America/Montreal" ) ) );
Instant instantClockSystemDefaultZone = Instant.now ( Clock.systemDefaultZone () );

instantClockSystemUTC.toString (): 2016-12-31T20: 52: 39.763Z

instantClockSystem.toString (): 2016-12-31T20: 52: 39.763Z

instantClockSystemDefaultZone.toString (): 2016-12-31T20: 52: 39.763Z

Standarduhr

Die standardmäßig für verwendete Implementierung Instant.nowist die von Clock.systemUTC(). Dies ist die Implementierung, die verwendet wird, wenn Sie a nicht angeben Clock. Überzeugen Sie sich selbst im Java 9-Quellcode vor der Veröffentlichung fürInstant.now .

public static Instant now() {
    return Clock.systemUTC().instant();
}

Der Standardwert Clockfür OffsetDateTime.nowund ZonedDateTime.nowist Clock.systemDefaultZone(). Siehe Quellcode .

public static ZonedDateTime now() {
    return now(Clock.systemDefaultZone());
}

Das Verhalten der Standardimplementierungen hat sich zwischen Java 8 und Java 9 geändert. In Java 8 wird der aktuelle Moment mit einer Auflösung nur in Millisekunden erfasst, obwohl die Klassen eine Auflösung von Nanosekunden speichern können . Java 9 bietet eine neue Implementierung, mit der der aktuelle Moment mit einer Auflösung von Nanosekunden erfasst werden kann - natürlich abhängig von der Leistungsfähigkeit Ihrer Computerhardware-Uhr.


Über java.time

Das java.time- Framework ist in Java 8 und höher integriert. Diese Klassen verdrängen die lästigen alten Legacy - Datum-Zeit - Klassen wie java.util.Date, Calendar, & SimpleDateFormat.

Das Joda-Time- Projekt, das sich jetzt im Wartungsmodus befindet , empfiehlt die Migration zu den Klassen java.time .

Weitere Informationen finden Sie im Oracle-Lernprogramm . Suchen Sie im Stapelüberlauf nach vielen Beispielen und Erklärungen. Die Spezifikation ist JSR 310 .

Sie können java.time- Objekte direkt mit Ihrer Datenbank austauschen . Verwenden Sie einen JDBC-Treiber, der mit JDBC 4.2 oder höher kompatibel ist . Keine Notwendigkeit für Zeichenfolgen, keine Notwendigkeit für java.sql.*Klassen.

Woher bekomme ich die java.time-Klassen?

Das ThreeTen-Extra- Projekt erweitert java.time um zusätzliche Klassen. Dieses Projekt ist ein Testfeld für mögliche zukünftige Ergänzungen von java.time. Sie können einige nützliche Klassen hier wie finden Interval, YearWeek, YearQuarter, und mehr .

Basil Bourque
quelle
42

Wie von Jon Skeet gesagt :

"use Joda Time" ist fast immer die beste Antwort auf alle Fragen zu "Wie erreiche ich X mit java.util.Date/Calendar?"

Also los (vorausgesetzt, Sie haben gerade alle Ihre new Date()durch ersetzt new DateTime().toDate())

//Change to specific time
DateTimeUtils.setCurrentMillisFixed(millis);
//or set the clock to be a difference from system time
DateTimeUtils.setCurrentMillisOffset(millis);
//Reset to system time
DateTimeUtils.setCurrentMillisSystem();

Wenn Sie eine Bibliothek mit einer Schnittstelle importieren möchten (siehe Jons Kommentar unten), können Sie einfach Prevaylers Uhr verwenden , die Implementierungen sowie die Standardschnittstelle bereitstellt. Das volle Glas ist nur 96kB groß, also sollte es nicht die Bank sprengen ...

Stephen
quelle
13
Nein, das würde ich nicht empfehlen - es bewegt Sie nicht zu einer wirklich austauschbaren Uhr. Sie können die gesamte Statik verwenden. Die Verwendung von Joda Time ist natürlich eine gute Idee, aber ich möchte trotzdem eine Clock-Oberfläche oder ähnliches verwenden.
Jon Skeet
@ Jon: Richtig - zum Testen ist es in Ordnung, aber es ist besser, eine Schnittstelle zu haben.
Stephen
5
@ Jon: Dies ist eine reibungslose und meiner Meinung nach perfekte und einfache Möglichkeit, wenn Sie zeitabhängigen Code testen möchten, der bereits JodaTime verwendet. Der Versuch, das Rad durch die Einführung einer weiteren Abstraktionsschicht / eines weiteren Abstraktions-Frameworks neu zu erfinden, ist nicht der richtige Weg, wenn mit JodaTime bereits alles für Sie gelöst wurde.
Stefan Haberl
2
@ JonSkeet: Dies ist nicht das, wonach Mike ursprünglich gefragt hat. Er wollte ein Tool zum Testen von zeitabhängigem Code und keine vollständig austauschbare Uhr im Produktionscode. Ich bevorzuge es, meine Architektur so komplex wie nötig, aber so einfach wie möglich zu halten. Eine zusätzliche Abstraktionsebene (dh Schnittstelle) hier einzuführen ist einfach nicht notwendig
Stefan Haberl
2
@StefanHaberl: Es gibt viele Dinge, die nicht unbedingt "notwendig" sind - aber ich schlage wirklich vor, eine bereits vorhandene Abhängigkeit explizit zu machen und isoliert leichter zu testen. Das Fälschen der Systemzeit mit Statik hat alle normalen Probleme der Statik - es ist ein globaler Zustand ohne guten Grund . Mike fragte nach einer Möglichkeit, testbaren Code zu schreiben, der das aktuelle Datum und die aktuelle Uhrzeit verwendete. Ich glaube immer noch, dass mein Vorschlag weitaus sauberer ist (und Abhängigkeiten klarer macht), als eine statische Aufladung effektiv zu verfälschen.
Jon Skeet
15

Die Verwendung eines DateFactory-Musters scheint zwar nett zu sein, deckt jedoch keine Bibliotheken ab, die Sie nicht steuern können. Stellen Sie sich die Validierungsanmerkung @Past vor, deren Implementierung auf System.currentTimeMillis basiert (es gibt solche).

Deshalb verwenden wir jmockit, um die Systemzeit direkt zu verspotten:

import mockit.Mock;
import mockit.MockClass;
...
@MockClass(realClass = System.class)
public static class SystemMock {
    /**
     * Fake current time millis returns value modified by required offset.
     *
     * @return fake "current" millis
     */
    @Mock
    public static long currentTimeMillis() {
        return INIT_MILLIS + offset + millisSinceClassInit();
    }
}

Mockit.setUpMock(SystemMock.class);

Da es nicht möglich ist, den ursprünglichen nicht verspotteten Wert von Millis zu erreichen, verwenden wir stattdessen einen Nano-Timer - dies hängt nicht mit der Wanduhr zusammen, aber die relative Zeit reicht hier aus:

// runs before the mock is applied
private static final long INIT_MILLIS = System.currentTimeMillis();
private static final long INIT_NANOS = System.nanoTime();

private static long millisSinceClassInit() {
    return (System.nanoTime() - INIT_NANOS) / 1000000;
}

Es gibt ein dokumentiertes Problem, dass mit HotSpot die Zeit nach einer Reihe von Anrufen wieder normal wird - hier ist der Problembericht: http://code.google.com/p/jmockit/issues/detail?id=43

Um dies zu überwinden, müssen wir eine bestimmte HotSpot-Optimierung aktivieren - JVM mit diesem Argument ausführen -XX:-Inline.

Dies ist zwar möglicherweise nicht perfekt für die Produktion, aber für Tests in Ordnung und für die Anwendung absolut transparent, insbesondere wenn DataFactory wirtschaftlich nicht sinnvoll ist und nur aufgrund von Tests eingeführt wird. Es wäre schön, eine integrierte JVM-Option zu haben, die zu einer anderen Zeit ausgeführt werden kann. Schade, dass dies ohne solche Hacks nicht möglich ist.

Die vollständige Geschichte finden Sie in meinem Blog-Beitrag hier: http://virgo47.wordpress.com/2012/06/22/changing-system-time-in-java/

Die komplette praktische Klasse SystemTimeShifter finden Sie in der Post. Die Klasse kann in Ihren Tests verwendet werden, oder sie kann sehr einfach als erste Hauptklasse vor Ihrer eigentlichen Hauptklasse verwendet werden, um Ihre Anwendung (oder sogar den gesamten Anwendungsserver) zu einem anderen Zeitpunkt auszuführen. Dies ist natürlich hauptsächlich zu Testzwecken gedacht, nicht für die Produktionsumgebung.

BEARBEITEN Juli 2014: JMockit hat sich in letzter Zeit stark verändert und Sie müssen JMockit 1.0 verwenden, um dies korrekt zu verwenden (IIRC). Kann definitiv nicht auf die neueste Version aktualisieren, bei der die Benutzeroberfläche völlig anders ist. Ich habe darüber nachgedacht, nur das Notwendige einzubringen, aber da wir dieses Ding in unseren neuen Projekten nicht brauchen, entwickle ich dieses Ding überhaupt nicht.

Jungfrau47
quelle
1
Die Tatsache, dass Sie die Zeit für jede Klasse ändern, geht in beide Richtungen. Wenn Sie beispielsweise nanoTime anstelle von currentTimeMillis verspotten, müssen Sie sehr vorsichtig sein, um java.util.concurrent nicht zu beschädigen. Als Faustregel gilt: Wenn eine Bibliothek eines Drittanbieters in Tests schwer zu verwenden ist, sollten Sie die Eingaben der Bibliothek nicht verspotten: Sie sollten die Bibliothek selbst verspotten.
Dan Berindei
1
Wir verwenden diese Technik für Tests, bei denen wir nichts anderes verspotten. Es ist ein Integrationstest und wir testen den gesamten Stack, ob er das tut, was er sollte, wenn eine Aktivität zum ersten Mal im Jahr stattfindet usw. Guter Punkt zu nanoTime, zum Glück müssen wir uns nicht über diesen lustig machen, da er keine Wanduhr hat Bedeutung überhaupt. Ich würde gerne Java mit zeitversetzten Funktionen sehen, da ich sie bereits mehr als einmal brauchte und es deshalb sehr unglücklich ist, mit der Systemzeit herumzuspielen.
Jungfrau47
7

Powermock funktioniert großartig. Ich habe es nur benutzt, um mich zu verspotten System.currentTimeMillis().

ReneS
quelle
Ich habe versucht, Powermock zu verwenden, und wenn ich es richtig verstanden hätte, würde es die Seite der Angerufenen nicht wirklich verspotten. Stattdessen findet es alle Anrufer und wendet dann den Mock an. Dies kann sehr zeitaufwändig sein, da Sie JEDE Klasse in Ihrem Klassenpfad überprüfen lassen müssen, wenn Sie wirklich sicher sein möchten, dass Ihre Anwendung in verschobener Zeit funktioniert. Korrigieren Sie mich, wenn ich mich in Bezug auf Powermocks Verspottung irre.
Jungfrau47
1
@ virgo47 Nein, du hast es falsch verstanden. PowerMock überprüft niemals "jede Klasse in Ihrem Klassenpfad". Es werden nur die Klassen überprüft, die Sie ausdrücklich überprüfen sollen (durch die @PrepareForTestAnmerkung zur Testklasse).
Rogério
1
@ Rogério - genau so verstehe ich es - ich sagte "du musst es zulassen ...". Wenn Sie einen Aufruf an eine System.currentTimeMillisbeliebige Stelle in Ihrem Klassenpfad (eine beliebige Bibliothek) erwarten, müssen Sie jede Klasse überprüfen. Das habe ich gemeint, sonst nichts. Der Punkt ist, dass Sie dieses Verhalten auf der Anruferseite verspotten ("Sie sagen es spezifisch"). Dies ist für einfache Tests in Ordnung, jedoch nicht für Tests, bei denen Sie nicht sicher sind, wie diese Methode von wo aufgerufen wird (z. B. komplexere Komponententests mit beteiligten Bibliotheken). Das bedeutet nicht, dass Powermock überhaupt falsch ist, sondern nur, dass Sie es nicht für diese Art von Test verwenden können.
Jungfrau47
1
@ virgo47 Ja, ich verstehe was du meinst. Ich dachte, Sie meinten, PowerMock müsste automatisch jede Klasse im Klassenpfad untersuchen, was offensichtlich absurd wäre. In der Praxis wissen wir jedoch bei Komponententests normalerweise, welche Klasse (n) die Zeit liest, sodass es kein Problem ist, sie anzugeben. Für Integrationstests kann die Anforderung in PowerMock, dass alle verwendeten Klassen angegeben werden, ein Problem sein.
Rogério
6

Verwenden Sie die aspektorientierte Programmierung (AOP, z. B. AspectJ), um die Systemklasse so zu verweben, dass ein vordefinierter Wert zurückgegeben wird, den Sie in Ihren Testfällen festlegen können.

Oder weben Sie die Anwendungsklassen, um den Aufruf an System.currentTimeMillis()oder zu new Date()einer anderen Dienstprogrammklasse umzuleiten .

Das Weben java.lang.*von Systemklassen ( ) ist jedoch etwas schwieriger, und Sie müssen möglicherweise das Offline-Weben für rt.jar durchführen und für Ihre Tests ein separates JDK / rt.jar verwenden.

Es wird als binäres Weben bezeichnet und es gibt auch spezielle Tools , um das Weben von Systemklassen durchzuführen und einige Probleme damit zu umgehen (z. B. funktioniert das Bootstrapping der VM möglicherweise nicht).

mhaller
quelle
Das funktioniert natürlich, aber mit einem Verspottungswerkzeug ist es viel einfacher als mit einem AOP-Werkzeug. Die letztere Art von Werkzeug ist für den fraglichen Zweck einfach zu allgemein.
Rogério
3

Es gibt wirklich keine Möglichkeit, dies direkt in der VM zu tun, aber Sie könnten alle etwas tun, um die Systemzeit auf dem Testcomputer programmgesteuert einzustellen. Die meisten (alle?) Betriebssysteme verfügen über Befehlszeilenbefehle, um dies zu tun.

Jeremy Raymond
quelle
Zum Beispiel unter Windows die Befehle dateund time. Unter Linux den dateBefehl.
Jeremy Raymond
Warum sollte es nicht möglich sein, dies mit AOP oder Instrumentalisierung zu tun (wurde das dort getan)?
jarnbjo
3

Eine funktionierende Methode zum Überschreiben der aktuellen Systemzeit für JUnit-Testzwecke in einer Java 8-Webanwendung mit EasyMock, ohne Joda Time und ohne PowerMock.

Folgendes müssen Sie tun:

Was ist in der getesteten Klasse zu tun?

Schritt 1

Fügen Sie java.time.Clockder getesteten Klasse ein neues Attribut hinzu MyServiceund stellen Sie sicher, dass das neue Attribut bei Standardwerten mit einem Instanziierungsblock oder einem Konstruktor ordnungsgemäß initialisiert wird:

import java.time.Clock;
import java.time.LocalDateTime;

public class MyService {
  // (...)
  private Clock clock;
  public Clock getClock() { return clock; }
  public void setClock(Clock newClock) { clock = newClock; }

  public void initDefaultClock() {
    setClock(
      Clock.system(
        Clock.systemDefaultZone().getZone() 
        // You can just as well use
        // java.util.TimeZone.getDefault().toZoneId() instead
      )
    );
  }
  { 
    initDefaultClock(); // initialisation in an instantiation block, but 
                        // it can be done in a constructor just as well
  }
  // (...)
}

Schritt 2

Fügen Sie das neue Attribut clockin die Methode ein, die ein aktuelles Datum und eine aktuelle Uhrzeit anfordert. In meinem Fall musste ich beispielsweise überprüfen, ob ein in der Datenbank gespeichertes Datum zuvor aufgetreten ist LocalDateTime.now(), das ich LocalDateTime.now(clock)wie folgt ersetzt habe:

import java.time.Clock;
import java.time.LocalDateTime;

public class MyService {
  // (...)
  protected void doExecute() {
    LocalDateTime dateToBeCompared = someLogic.whichReturns().aDate().fromDB();
    while (dateToBeCompared.isBefore(LocalDateTime.now(clock))) {
      someOtherLogic();
    }
  }
  // (...) 
}

Was ist in der Testklasse zu tun?

Schritt 3

Erstellen Sie in der Testklasse ein Mock-Clock-Objekt, fügen Sie es in die Instanz der getesteten Klasse ein, bevor Sie die getestete Methode aufrufen doExecute(), und setzen Sie es anschließend wie folgt zurück:

import java.time.Clock;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import org.junit.Test;

public class MyServiceTest {
  // (...)
  private int year = 2017;
  private int month = 2;
  private int day = 3;

  @Test
  public void doExecuteTest() throws Exception {
    // (...) EasyMock stuff like mock(..), expect(..), replay(..) and whatnot

    MyService myService = new MyService();
    Clock mockClock =
      Clock.fixed(
        LocalDateTime.of(year, month, day, 0, 0).toInstant(OffsetDateTime.now().getOffset()),
        Clock.systemDefaultZone().getZone() // or java.util.TimeZone.getDefault().toZoneId()
      );
    myService.setClock(mockClock); // set it before calling the tested method

    myService.doExecute(); // calling tested method 

    myService.initDefaultClock(); // reset the clock to default right afterwards with our own previously created method

    // (...) remaining EasyMock stuff: verify(..) and assertEquals(..)
    }
  }

Überprüfen Sie es im Debug-Modus und Sie werden sehen, dass das Datum des 3. Februar 2017 korrekt in die myServiceInstanz eingefügt und in der Vergleichsanweisung verwendet wurde und dann ordnungsgemäß auf das aktuelle Datum mit zurückgesetzt wurde initDefaultClock().

KiriSakow
quelle
2

Meiner Meinung nach kann nur eine nicht-invasive Lösung funktionieren. Besonders wenn Sie externe Bibliotheken und eine große Legacy-Codebasis haben, gibt es keine zuverlässige Möglichkeit, die Zeit zu verspotten.

JMockit ... funktioniert nur für eine begrenzte Anzahl von Malen

PowerMock & Co ... muss die Clients auf System.currentTimeMillis () verspotten . Wieder eine invasive Option.

Daraus ergibt sich nur, dass der erwähnte Javaagent- oder AOP- Ansatz für das gesamte System transparent ist. Hat jemand das getan und könnte auf eine solche Lösung hinweisen?

@jarnbjo: Könnten Sie bitte einen Teil des Javaagent-Codes anzeigen?

user1477398
quelle
3
jmockit-Lösung, die unbegrenzt oft mit einem kleinen -XX: -Inline-Hack (auf Suns HotSpot) funktioniert : virgo47.wordpress.com/2012/06/22/changing-system-time-in-java Die komplette praktische Klasse SystemTimeShifter wird bereitgestellt in der Post. Die Klasse kann in Ihren Tests verwendet werden, oder sie kann sehr einfach als erste Hauptklasse vor Ihrer eigentlichen Hauptklasse verwendet werden, um Ihre Anwendung (oder sogar den gesamten Anwendungsserver) zu einem anderen Zeitpunkt auszuführen. Dies ist natürlich hauptsächlich zu Testzwecken gedacht, nicht für die Produktionsumgebung.
Jungfrau47
2

Wenn Sie Linux ausführen, können Sie den Hauptzweig von libfaketime oder zum Zeitpunkt des Testens Commit 4ce2835 verwenden .

Stellen Sie einfach die Umgebungsvariable mit der Zeit ein, mit der Sie Ihre Java-Anwendung verspotten möchten, und führen Sie sie mit ld-pretoading aus:

# bash
export FAKETIME="1985-10-26 01:21:00"
export DONT_FAKE_MONOTONIC=1
LD_PRELOAD=/usr/local/lib/faketime/libfaketimeMT.so.1 java -jar myapp.jar

Die zweite Umgebungsvariable ist für Java-Anwendungen von größter Bedeutung, die sonst einfrieren würden. Es erfordert den Hauptzweig von libfaketime zum Zeitpunkt des Schreibens.

Wenn Sie die Zeit eines von systemd verwalteten Dienstes ändern möchten, fügen Sie einfach Folgendes zu Ihren Überschreibungen von Gerätedateien hinzu, z. B. für elasticsearch /etc/systemd/system/elasticsearch.service.d/override.conf:

[Service]
Environment="FAKETIME=2017-10-31 23:00:00"
Environment="DONT_FAKE_MONOTONIC=1"
Environment="LD_PRELOAD=/usr/local/lib/faketime/libfaketimeMT.so.1"

Vergessen Sie nicht, systemd mit `systemctl daemon-reload neu zu laden

Faxmodem
quelle
-1

Wenn Sie die Methode mit System.currentTimeMillis()Argument verspotten möchten, können Sie die anyLong()Matchers-Klasse als Argument übergeben.

PS: Ich kann meinen Testfall mit dem oben genannten Trick erfolgreich ausführen und nur weitere Details zu meinem Test mitteilen, wenn ich PowerMock- und Mockito-Frameworks verwende.

Paresh Mayani
quelle