Remplacer le système Java.currentTimeMillis pour tester le code sensible au temps


Existe-t-il un moyen, en code ou avec des arguments JVM, de remplacer l'heure actuelle, telle que présentée via System.currentTimeMillis, autre que de changer manuellement l'horloge système sur la machine hôte?

Un peu de fond:

Nous avons un système qui exécute un certain nombre de tâches comptables qui tournent une grande partie de leur logique autour de la date actuelle (ie 1er du mois, 1er de l'année, etc.)

Malheureusement, beaucoup de code hérité appelle des fonctions telles que new Date() ou Calendar.getInstance(), qui finissent par appelez vers le bas à System.currentTimeMillis.

À des fins de test, en ce moment, nous sommes coincés avec la mise à jour manuelle de l'horloge système pour manipuler l'heure et la date à laquelle le code pense que le test est en cours d'exécution.

Donc ma question est:

Existe-t-il un moyen de remplacer ce qui est renvoyé par System.currentTimeMillis? Par exemple, pour dire à la JVM d'ajouter ou de soustraire automatiquement un décalage avant de revenir de cette méthode?

Merci d'avance!

Author: Ashish Kumar, 2010-01-04

12 answers

Je fortement recommande qu'au lieu de jouer avec l'horloge système, vous mordez la balle et refactorisez ce code hérité pour utiliser une horloge remplaçable. Idéalement cela devrait être fait avec l'injection de dépendance, mais même si vous utilisiez un singleton remplaçable, vous gagneriez en testabilité.

Cela pourrait presque être automatisé avec search and replace pour la version singleton:

  • Remplacer Calendar.getInstance() avec Clock.getInstance().getCalendarInstance().
  • Remplacer new Date() par Clock.getInstance().newDate()
  • Remplacer System.currentTimeMillis() avec Clock.getInstance().currentTimeMillis()

(etc selon les besoins)

Une fois que vous avez fait cette première étape, vous pouvez remplacer le singleton par DI un peu à la fois.

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

Tl; dr

Existe-t-il un moyen, soit dans le code, soit avec des arguments JVM, de remplacer l'heure actuelle, telle que présentée via le système.currentTimeMillis, autre que de changer manuellement l'horloge système sur la machine hôte?

Oui.

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

Clock En java.temps

Nous avons une nouvelle solution au problème d'un remplacement d'horloge enfichable pour faciliter les tests avec faux valeurs date-heure. Le java.le paquet de temps dans Java 8 comprend une classe abstraite java.time.Clock, avec un but explicite:

Pour permettre à d'autres horloges d'être branchées au fur et à mesure des besoins

Vous pouvez brancher votre propre implémentation de Clock, bien que vous puissiez probablement en trouver une déjà conçue pour répondre à vos besoins. Pour votre commodité, java.time inclut des méthodes statiques pour produire des implémentations spéciales. Ces implémentations alternatives peuvent être utiles lors des tests.

Cadence modifiée

Le diverses tick… méthodes produisent des horloges qui incrémentent le moment actuel avec une cadence différente.

La valeur par défaut Clockindique un temps mis à jour aussi fréquemment que millisecondesen Java 8 et en Java 9 aussi bien que nanosecondes (en fonction de votre matériel). Vous pouvez demander que le vrai moment actuel soit signalé avec une granularité différente.

Fausses horloges

Certaines horloges peuvent mentir, produisant un résultat différent de celui de l'horloge matérielle du système d'exploitation hôte.

  • fixed - Signale un seul moment immuable (non incrémenté) comme moment actuel.
  • offset - les Rapports à l'heure actuelle, mais décalée par le passé Duration argument.

Pour exemple, verrouillez le premier moment du premier Noël de cette année. en d'autres termes, lorsque Le Père Noël et ses rennes font leur premier arrêt. Le premier fuseau horaire de nos jours semble être Pacific/Kiritimati à +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 );

Utilisez cette horloge fixe spéciale pour toujours retourner le même moment. Nous obtenons le premier moment du jour de Noël dans Kiritimati , avec UTC montrant un heure d'horloge murale de quatorze heures plus tôt, 10 heures sur la date antérieure du 24 Décembre.

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

Éphémère.toString (): 2016-12-24T10:00: 00Z

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

Voir code en direct dans IdeOne.com .

Heure vraie, fuseau horaire différent

Vous pouvez contrôler le fuseau horaire attribué par l'implémentation Clock. Cela pourrait être utile dans certains tests. Mais je ne recommande pas cela dans le code de production, où vous devez toujours spécifier explicitement les arguments optionnels ZoneId ou ZoneOffset.

Vous pouvez spécifier que UTC est la zone par défaut.

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

Vous pouvez spécifier n'importe quel fuseau horaire particulier. Spécifier un fuseau horaire nom dans le format continent/region, tels que America/Montreal, Africa/Casablanca, ou Pacific/Auckland. N'utilisez jamais l'abréviation de 3-4 lettres telle que EST ou ISTcar ce sont pas de vrais fuseaux horaires, non normalisés et même pas uniques(!).

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

Vous pouvez spécifier Le fuseau horaire par défaut actuel de la JVM doit être celui par défaut d'un objet Clock particulier.

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

Exécutez ce code pour comparer. Notez qu'ils rapportent tous le même moment, le même point sur la chronologie. Ils ne diffèrent que dans horloge murale; en d'autres termes, trois façons de dire la même chose, trois façons d'afficher le même moment.

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

America/Los_Angeles était la zone par défaut actuelle de la JVM sur l'ordinateur qui exécutait ce code.

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

ZdtClockSystem.toString (): 2016-12-31T15:52: 39.750-05: 00[Amérique/Montréal]

ZdtClockSystemDefaultZone.toString (): 2016-12-31T12:52:39.762-08: 00[Amérique/Los_Angeles]

Le Instant la classe est toujours en UTC, par définition. Donc, ces trois utilisations liées à la zone Clock ont exactement le même effet.

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

Horloge par défaut

L'implémentation utilisée par défaut pour Instant.now est celle renvoyée par Clock.systemUTC(). C'est l'implémentation utilisée lorsque vous ne spécifiez pas un Clock. Voyez par vous-même dans pré-version de Java 9 code source Instant.now.

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

La valeur par défaut Clock pour OffsetDateTime.now et ZonedDateTime.now est Clock.systemDefaultZone(). Voir code source .

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

Le comportement des implémentations par défaut a changé entre Java 8 et Java 9. Dans Java 8, le moment actuel est capturé avec une résolution uniquement en millisecondes malgré la capacité des classes à stocker une résolution de nanosecondes. Java 9 apporte une nouvelle implémentation capable de capturer le moment actuel avec une résolution de nanosecondes – en fonction, bien sûr, de la capacité de votre horloge matérielle informatique.


À propos de java.temps

Le java.temps framework est intégré à Java 8 et versions ultérieures. Ces classes permettent d'éviter la pénible vieux héritage date-heure classes telles que java.util.Date, Calendar, & SimpleDateFormat.

Le Joda-Temps le projet, maintenant en mode de maintenance, conseille la migration vers le java.temps classes.

Pour en savoir plus, voir le Tutoriel Oracle. Et rechercher Stack Overflow pour de nombreux exemples et explications. La spécification est JSR 310.

, Vous pouvez échanger java.temps objets directement avec votre base de données. Utilisez un pilote JDBC compatible avec JDBC 4.2 ou version ultérieure. Pas besoin de chaînes, pas besoin de classes java.sql.*.

Où obtenir le java.les classes de temps?

  • Java SE 8, Java SE 9, et plus tard
    • Intégré.
    • Partie de l'API Java standard avec une implémentation groupée.
    • Java 9 ajoute quelques fonctionnalités et corrections mineures.
  • Java SE 6 et Java SE 7
    • Une grande partie du java.la fonctionnalité time est back-ported vers Java 6 & 7 dans ThreeTen-Backport.
  • Android
    • Versions ultérieures des implémentations de bundle Android du java.les classes de temps.
    • Pour Android antérieur (Troisabp le projet adapte ThreeTen-Backport (mentionné ci-dessus). Voir Comment utiliser ThreeTenABP....

Le Trois-Extra le projet étend java.temps avec des cours supplémentaires. Ce projet est un terrain d'essai pour d'éventuels ajouts futurs à java.temps. Vous pouvez trouver quelques classes utiles ici comme Interval, YearWeek, YearQuarter, et plus.

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

Comme dit par Jon Skeet :

"utilisation Joda Time" est presque toujours la meilleure réponse à toute question impliquant "comment puis-je obtenir X avec java.util.Date / Calendrier?"

Alors voici (en supposant que vous venez de remplacer tous vos new Date() par 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();

Si vous voulez importer une bibliothèque qui a une interface (voir le commentaire de Jon ci-dessous), vous pouvez simplement utiliser Horloge de Prevayler, qui fournira des implémentations ainsi que l'interface standard. Le le plein pot est seulement 96kB, ainsi il ne devrait pas casser la banque...

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

Bien que l'utilisation d'un modèle DateFactory semble agréable, elle ne couvre pas les bibliothèques que vous ne pouvez pas contrôler - imaginez l'annotation de validation @Past avec une implémentation reposant sur le système.currentTimeMillis (il y a une telle).

C'est pourquoi nous utilisons jmockit pour nous moquer directement de l'heure système:

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

Parce qu'il n'est pas possible d'obtenir la valeur non verrouillée d'origine de millis, nous utilisons nano timer à la place - ce n'est pas lié à l'horloge murale, mais le temps relatif suffit ici:

// 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à est un problème documenté, qu'avec HotSpot le temps revient à la normale après un certain nombre d'appels-voici le rapport de problème: http://code.google.com/p/jmockit/issues/detail?id=43

Pour surmonter cela, nous devons activer une optimisation de HotSpot spécifique - exécuter la JVM avec cet argument -XX:-Inline.

Bien que cela ne soit pas parfait pour la production, c'est très bien pour les tests et c'est absolument transparent pour l'application, en particulier lorsque DataFactory n'a pas de sens commercial et est introduit uniquement à cause des tests. Ce serait bien d'avoir l'option JVM intégrée pour s'exécuter à un moment différent, dommage que ce ne soit pas possible sans hacks comme celui-ci.

L'histoire complète est dans mon article de blog ici: http://virgo47.wordpress.com/2012/06/22/changing-system-time-in-java/

Complete handy class SystemTimeShifter est fourni dans le post. La classe peut être utilisée dans vos tests, ou il peut être utilisé comme la première classe principale avant votre véritable classe principale très facilement afin de s'exécuter votre application (ou même tout appserver) dans un temps différent. Bien sûr, cela est destiné à des fins de test principalement, pas pour l'environnement de production.

EDIT juillet 2014: JMockit a beaucoup changé ces derniers temps et vous êtes obligé d'utiliser JMockit 1.0 pour l'utiliser correctement (IIRC). Je ne peux certainement pas passer à la version la plus récente où l'interface est complètement différente. Je pensais à inlining juste les choses nécessaires, mais comme nous n'avons pas besoin de cette chose dans nos nouveaux projets, je ne développe pas cette chose à tous.

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

Powermock fonctionne très bien. Je l'ai juste utilisé pour me moquer de System.currentTimeMillis().

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

Utilisez la programmation orientée Aspect (AOP, par exemple AspectJ) pour tisser la classe Système pour renvoyer une valeur prédéfinie que vous pouvez définir dans vos cas de test.

Ou tissez les classes d'application pour rediriger l'appel vers System.currentTimeMillis() ou vers new Date() vers une autre classe utilitaire de votre choix.

Weaving system classes (java.lang.*) est cependant un peu plus délicat et vous devrez peut-être effectuer un tissage hors ligne pour rt.jar et utilisez un séparé JDK/rt.jar pour vos tests.

Il s'appelle Tissage binaire et il existe également des outils spéciaux pour effectuer le tissage des classes système et contourner certains problèmes avec cela (par exemple, l'amorçage de la machine virtuelle peut ne pas fonctionner)

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

Il n'y a vraiment pas de moyen de le faire directement dans la machine virtuelle, mais vous pouvez tous définir par programme l'heure système sur la machine de test. La plupart (toutes?) OS ont des commandes de ligne de commande pour ce faire.

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

Sans faire de ré-affacturage, pouvez-vous tester run dans une instance de machine virtuelle OS?

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

À mon avis, seule une solution non invasive peut fonctionner. Surtout si vous avez des bibliothèques externes et une grande base de code héritée, il n'y a pas de moyen fiable de se moquer du temps.

JMockit ... fonctionne uniquement pour un nombre restreint de fois

PowerMock & Co ...doit se moquer desclients au système.currentTimeMillis (). Encore une envahissantes option.

De cela, je ne vois que l'approche javaagent ou aop mentionnée étant transparente pour l'ensemble système. Quelqu'un a-t-il fait cela et pourrait-il indiquer une telle solution?

@jarnbjo: pourriez-vous montrer une partie du code javaagent s'il vous plait?

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

Un moyen efficace de remplacer l'heure système actuelle à des fins de test JUnit dans une application Web Java 8 avec EasyMock, sans heure Joda et sans PowerMock.

Voici ce que vous devez faire:

Ce qui doit être fait dans la classe testée

Étape 1

Ajoutez un nouvel attribut java.time.Clock à la classe testée {[4] } et assurez-vous que le nouvel attribut sera initialisé correctement aux valeurs par défaut avec un bloc d'instanciation ou un constructeur:

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

Étape 2

Injectez le nouvel attribut clock dans la méthode qui appelle une date-heure actuelle. Par exemple, dans mon cas, j'ai dû vérifier si une date stockée dans dataase s'était produite avant LocalDateTime.now(), que j'ai remplacé par LocalDateTime.now(clock), comme ceci:

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

Ce qui doit être fait dans la classe de test

Étape 3

Dans la classe test, créez un objet clock simulé et injectez - le dans l'instance de la classe testée juste avant d'appeler la méthode testée doExecute(), puis réinitialisez-le juste après, comme ceci:

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

Vérifiez-le en mode débogage et vous verrez que la date du 3 février 2017 a été correctement injectée dans l'instance myService et utilisée dans l'instruction de comparaison, puis a été correctement réinitialisée à la date actuelle avec initDefaultClock().

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

Si vous utilisez Linux, vous pouvez utiliser la branche principale de libfaketime, ou au moment du test commit 4ce2835.

Définissez simplement la variable d'environnement avec le temps avec lequel vous souhaitez vous moquer de votre application java et exécutez-la en utilisant 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 deuxième variable d'environnement est primordiale pour les applications java, qui autrement gèleraient. Il nécessite la branche principale de libfaketime au moment de la rédaction.

Si vous souhaitez changer l'heure d'un service géré systemd, ajoutez simplement ce qui suit à vos remplacements de fichier d'unité, par exemple pour elasticsearch ce serait /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"

N'oubliez pas de recharger systemd en utilisant ' systemctl daemon-reload

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

Si vous voulez vous moquer de la méthode ayant System.currentTimeMillis() argument, alors vous pouvez passer anyLong() de Rapprochement de la classe comme un argument.

PS Je suis en mesure d'exécuter mon cas de test avec succès en utilisant l'astuce ci-dessus et juste pour partager plus de détails sur mon test que j'utilise les frameworks PowerMock et Mockito.

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