Sistema Java Override.currentTimeMillis per testare il codice sensibile al tempo


Esiste un modo, in codice o con argomenti JVM, per sovrascrivere l'ora corrente, come presentato tramite System.currentTimeMillis, oltre a modificare manualmente l'orologio di sistema sulla macchina host?

Un po ' di sfondo:

Abbiamo un sistema che esegue una serie di lavori contabili che ruotano gran parte della loro logica attorno alla data corrente (cioè 1st del mese, 1st dell'anno, ecc.)

Sfortunatamente, un sacco di codice legacy chiama funzioni come new Date() o Calendar.getInstance(), entrambe le quali alla fine richiamare a System.currentTimeMillis.

A scopo di test, in questo momento, siamo bloccati con l'aggiornamento manuale dell'orologio di sistema per manipolare l'ora e la data in cui il codice pensa che il test sia in esecuzione.

Quindi la mia domanda è:

Esiste un modo per ignorare ciò che viene restituito da System.currentTimeMillis? Ad esempio, per dire alla JVM di aggiungere o sottrarre automaticamente alcuni offset prima di tornare da quel metodo?

Grazie in anticipo!

Author: Ashish Kumar, 2010-01-04

12 answers

I fortemente consiglia che invece di fare scherzi con l'orologio di sistema, si mordere il proiettile e refactoring che il codice legacy di utilizzare un orologio sostituibile. Idealmente dovrebbe essere fatto con l'iniezione di dipendenza, ma anche se si utilizzasse un singleton sostituibile si otterrebbe la testabilità.

Questo potrebbe essere quasi automatizzato con search and replace per la versione singleton:

  • Sostituire Calendar.getInstance() con Clock.getInstance().getCalendarInstance().
  • Sostituire new Date() con Clock.getInstance().newDate()
  • Sostituire System.currentTimeMillis() con Clock.getInstance().currentTimeMillis()

(ecc come richiesto)

Una volta fatto questo primo passo, è possibile sostituire il singleton con DI un po ' alla volta.

 91
Author: Jon Skeet, 2010-01-04 19:43:46

Tl;dr

Esiste un modo, nel codice o con argomenti JVM, per sovrascrivere l'ora corrente, come presentato tramite System.currentTimeMillis, oltre a modificare manualmente l'orologio di sistema sulla macchina host?

Sì.

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

Clock In java.tempo

Abbiamo una nuova soluzione al problema di una sostituzione dell'orologio collegabile per facilitare il test con faux valori data-ora. Il java.tempo pacchetto in Java 8 include una classe astrattajava.time.Clock, con uno scopo esplicito:

Per consentire agli orologi alternativi di essere collegati come e quando richiesto

È possibile collegare la propria implementazione di Clock, anche se è probabile che si possa trovare uno già fatto per soddisfare le vostre esigenze. Per tua comodità, java.il tempo include metodi statici per produrre implementazioni speciali. Queste implementazioni alternative possono essere preziose durante i test.

Cadenza alterata

Il vari metodi tick… producono orologi che incrementano il momento corrente con una cadenza diversa.

Il valore predefinito Clockriporta un tempo aggiornato con una frequenza pari a millisecondiin Java 8 e in Java 9 fino a nanosecondi (a seconda dell'hardware). Puoi chiedere che il vero momento corrente venga segnalato con una granularità diversa.

Falsi orologi

Alcuni orologi possono mentire, producendo un risultato diverso da quello dell'orologio hardware del sistema operativo host.

  • fixed - Riporta un singolo momento immutabile (non incrementale) come momento corrente.
  • offset - Riporta il momento corrente ma spostato dal passato Duration argomento.

Anziché esempio, bloccare nel primo momento del primo Natale di quest'anno. in altre parole, quando Babbo Natale e le sue renne fanno la loro prima fermata . Il primo fuso orario al giorno d'oggi sembra essere Pacific/Kiritimati a +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 );

Usa quello speciale orologio fisso per restituire sempre lo stesso momento. Otteniamo il primo momento del giorno di Natale in Kiritimati, con UTC che mostra un orologio da parete tempo di quattordici ore prima, 10 del mattino la data precedente del 24 Dicembre.

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

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

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

Vedi codice live in IdeOne.com .

Tempo reale, fuso orario diverso

È possibile controllare il fuso orario assegnato dall'implementazione Clock. Questo potrebbe essere utile in alcuni test. Ma non lo consiglio nel codice di produzione, dove dovresti sempre specificare esplicitamente gli argomenti opzionali ZoneId o ZoneOffset.

È possibile specificare che UTC sia la zona predefinita.

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

È possibile specificare un particolare fuso orario. Specificare un nome del fuso orario corretto nel formato continent/region, ad esempio America/Montreal, Africa/Casablanca, oppure Pacific/Auckland. Non usare mai l'abbreviazione di 3-4 lettere come EST o ISTpoiché sono non fusi orari veri, non standardizzati e nemmeno unici(!).

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

È possibile specificare Il fuso orario predefinito corrente di JVM dovrebbe essere quello predefinito per un particolare oggetto Clock.

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

Esegui questo codice per confrontarlo. Nota che riportano tutti lo stesso momento, lo stesso punto sulla timeline. Differiscono solo in tempo dell'orologio da parete ; in altre parole, tre modi per dire la stessa cosa, tre modi per visualizzare lo stesso momento.

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

America/Los_Angeles era la zona predefinita corrente JVM sul computer che eseguiva questo codice.

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

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]

Il Instant la classe è sempre in UTC per definizione. Quindi questi tre usi correlati alla zona Clock hanno esattamente lo stesso effetto.

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.763 Z

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

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

Orologio predefinito

L'implementazione utilizzata di default per Instant.now è quella restituita da Clock.systemUTC(). Questa è l'implementazione utilizzata quando non si specifica un Clock. Vedere di persona in pre-release Java 9 codice sorgente per Instant.now.

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

Il valore predefinito Clock per OffsetDateTime.now e ZonedDateTime.now è Clock.systemDefaultZone(). Vedere codice sorgente .

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

Il comportamento delle implementazioni predefinite è cambiato tra Java 8 e Java 9. In Java 8, il momento corrente viene catturato con una risoluzione solo in millisecondi nonostante la capacità delle classi di memorizzare una risoluzione di nanosecondi. Java 9 porta una nuova implementazione in grado di catturare il momento corrente con una risoluzione di nanosecondi – a seconda, naturalmente, sulla capacità del vostro orologio hardware del computer.


Informazioni su java.tempo

Il java.tempo framework è integrato in Java 8 e versioni successive. Queste classi soppiantano le vecchie e fastidiose legacy classi data-ora come java.util.Date, Calendar, & SimpleDateFormat.

Il Joda-Tempo progetto, ora in modalità di manutenzione , consiglia la migrazione al java.tempo classi.

Per saperne di più, vedere il Tutorial Oracle. E cerca Stack Overflow per molti esempi e spiegazioni. La specifica è JSR 310.

È possibile scambiare java.tempo oggetti direttamente con il database. Utilizzare un driver JDBC compatibile con JDBC 4.2 o successivo. Non c'è bisogno di stringhe, non c'è bisogno di java.sql.* classi.

Dove ottenere il java.lezioni di tempo?

  • Java SE 8, Java SE 9, e dopo
    • Integrato.
    • Parte dell'API Java standard con un'implementazione in bundle.
    • Java 9 aggiunge alcune funzionalità e correzioni minori.
  • Java SE 6 e Java SE 7
    • Gran parte del java.la funzionalità time è back-porting su Java 6 e 7 in Treten-Backport.
  • Android
    • Versioni successive di Android bundle implementazioni di java.lezioni di tempo.
    • Per Android precedente (TRETENABP progetto adatta ThreeTen-Backport (menzionato sopra). Vedi Come usare ThreeTenABP....

Il Tre-Extra progetto estende java.tempo con classi aggiuntive. Questo progetto è un terreno di prova per possibili future aggiunte a java.tempo. Si possono trovare alcune classi utili qui come Interval, YearWeek, YearQuarter, e più .

 47
Author: Basil Bourque, 2018-03-16 20:27:23

Come detto da Jon Skeet:

"usa Joda Time" è quasi sempre la migliore risposta a qualsiasi domanda che coinvolga "come faccio a raggiungere X con java.util.Data / Calendario?"

Quindi qui va (presumendo che tu abbia appena sostituito tutti i tuoi new Date() con 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();

Se vuoi importare una libreria con un'interfaccia (vedi il commento di Jon sotto), puoi semplicemente usare Prevayler's Clock, che fornirà implementazioni e l'interfaccia standard. Il il barattolo pieno è solo 96kB, quindi non dovrebbe rompere la banca...

 40
Author: Stephen, 2010-01-04 21:39:10

Mentre l'utilizzo di alcuni pattern DateFactory sembra bello, non copre le librerie che non puoi controllare - immagina l'annotazione di convalida @Past con l'implementazione basata sul Sistema.currentTimeMillis (c'è tale).

Ecco perché usiamo jmockit per prendere in giro il tempo di sistema direttamente:

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);

Poiché non è possibile ottenere il valore originale non bloccato di millis, usiamo invece nano timer - questo non è correlato all'orologio da parete, ma il tempo relativo è sufficiente qui:

// 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;
}

Lì è un problema documentato, che con HotSpot il tempo torna alla normalità dopo un certo numero di chiamate-ecco il rapporto sul problema: http://code.google.com/p/jmockit/issues/detail?id=43

Per superare questo dobbiamo attivare una specifica ottimizzazione HotSpot - eseguire JVM con questo argomento -XX:-Inline.

Anche se questo potrebbe non essere perfetto per la produzione, va bene per i test ed è assolutamente trasparente per l'applicazione, specialmente quando DataFactory non ha senso per gli affari e viene introdotto solo a causa di test. Sarebbe bello avere l'opzione JVM integrata da eseguire in tempi diversi, peccato che non sia possibile senza hack come questo.

La storia completa è nel mio post sul blog qui: http://virgo47.wordpress.com/2012/06/22/changing-system-time-in-java /

Completa classe SystemTimeShifter a portata di mano è fornito nel post. La classe può essere utilizzata nei test o può essere utilizzata come prima classe principale prima della tua vera classe principale molto facilmente per essere eseguita la tua applicazione (o anche l'intero appserver) in un momento diverso. Naturalmente, questo è inteso principalmente a scopo di test, non per l'ambiente di produzione.

MODIFICA luglio 2014: JMockit è cambiato molto ultimamente e sei obbligato a usare JMockit 1.0 per usarlo correttamente (IIRC). Sicuramente non è possibile aggiornare alla versione più recente in cui l'interfaccia è completamente diversa. Stavo pensando di inlining solo le cose necessarie, ma come non abbiamo bisogno di questa cosa nei nostri nuovi progetti non sto sviluppando questa cosa affatto.

 12
Author: virgo47, 2014-07-14 09:52:34

Powermock funziona alla grande. L'ho appena usato per deridere System.currentTimeMillis().

 7
Author: ReneS, 2016-05-11 12:51:43

Utilizzare la programmazione orientata agli aspetti (AOP, ad esempio AspectJ) per tessere la classe di sistema per restituire un valore predefinito che è possibile impostare nei casi di test.

O tessere le classi dell'applicazione per reindirizzare la chiamata a System.currentTimeMillis() o a new Date() a un'altra classe di utilità propria.

Weaving system classes (java.lang.*) è tuttavia un po ' più complicato e potrebbe essere necessario eseguire la tessitura offline per rt.jar e usa un separato JDK/rt.jar per i tuoi test.

Si chiama Tessitura binaria e ci sono anche speciali strumenti per eseguire la tessitura di classi di sistema e aggirare alcuni problemi con questo (ad esempio il bootstrap della VM potrebbe non funzionare)

 5
Author: mhaller, 2010-01-04 19:49:01

Non c'è davvero un modo per farlo direttamente nella VM, ma si potrebbe tutto qualcosa per impostare programmaticamente l'ora di sistema sulla macchina di test. La maggior parte (tutti?) Il sistema operativo ha comandi da riga di comando per farlo.

 3
Author: Jeremy Raymond, 2010-01-04 19:43:30

Senza fare il ri-factoring, è possibile eseguire il test in un'istanza di macchina virtuale del sistema operativo?

 3
Author: Jé Queue, 2010-01-04 21:18:15

A mio parere solo una soluzione non invasiva può funzionare. Soprattutto se hai librerie esterne e una grande base di codice legacy non esiste un modo affidabile per prendere in giro il tempo.

JMockit ... funziona solo per un numero limitato di volte

Per maggiori informazioni: ..ha bisogno di prendere in giro il client al sistema.currentTimeMillis (). Ancora una volta un'opzione invasiva.

Da questo vedo solo l'approccio javaagent o aop trasparente al tutto sistema. Qualcuno l'ha fatto e potrebbe indicare una soluzione del genere?

@ jarnbjo: potresti mostrare un po ' del codice javaagent per favore?

 2
Author: user1477398, 2012-06-23 22:46:32

Un modo di lavoro per ignorare il tempo di sistema corrente per scopi di test JUnit in un'applicazione Web Java 8 con EasyMock, senza Tempo Joda e senza PowerMock.

Ecco cosa devi fare:

Cosa deve essere fatto nella classe testata

Fase 1

Aggiungi un nuovo attributo java.time.Clock alla classe testata MyService e assicurati che il nuovo attributo venga inizializzato correttamente ai valori predefiniti con un blocco di istanziazione o un costruttore:

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
  }
  // (...)
}

Passo 2

Inserire il nuovo attributo clock nel metodo che richiede una data-ora corrente. Ad esempio, nel mio caso ho dovuto eseguire un controllo se una data memorizzata in dataase è avvenuta prima di LocalDateTime.now(), che ho rimontato con LocalDateTime.now(clock), in questo modo:

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();
    }
  }
  // (...) 
}

Cosa deve essere fatto nella classe di test

Fase 3

Nella classe test, creare un oggetto orologio finto e iniettarlo nell'istanza della classe testata appena prima di chiamare il metodo testato doExecute(), quindi ripristinarlo subito dopo, in questo modo:

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(..)
    }
  }

Controllalo in modalità debug e vedrai che la data del 3 febbraio 2017 è stata iniettata correttamente nell'istanza myService e utilizzata nell'istruzione di confronto, quindi è stata ripristinata correttamente alla data corrente con initDefaultClock().

 2
Author: KiriSakow, 2017-08-23 07:22:55

Se si sta eseguendo Linux, è possibile utilizzare il ramo master di libfaketime, o al momento del test commit 4ce2835.

Imposta semplicemente la variabile di ambiente con il tempo che desideri prendere in giro la tua applicazione java ed eseguila usando ld-preloading:

# 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

La seconda variabile d'ambiente è fondamentale per le applicazioni java, che altrimenti si bloccherebbero. Richiede il ramo master di libfaketime al momento della scrittura.

Se si desidera modificare l'ora di un servizio gestito systemd, basta aggiungere quanto segue alle sostituzioni del file di unità, ad esempio per elasticsearch questo sarebbe /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"

Non dimenticare di ricaricare systemd usando ' systemctl daemon-reload

 1
Author: swissunix, 2017-11-03 09:41:45

Se vuoi prendere in giro il metodo con l'argomento System.currentTimeMillis(), puoi passare anyLong() di classe Matchers come argomento.

PS Sono in grado di eseguire il mio test case con successo usando il trucco sopra e solo per condividere maggiori dettagli sul mio test che sto usando i framework PowerMock e Mockito.

 -1
Author: Paresh Mayani, 2015-08-12 10:12:31