Remplacer Java System.currentTimeMillis pour tester le code sensible au temps

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

Un petit fond:

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

Malheureusement, beaucoup d’appels de code hérités utilisent des fonctions telles que new Date() ou Calendar.getInstance() , qui appellent finalement System.currentTimeMillis .

À des fins de test, en ce moment, nous sums obligés de mettre à jour manuellement l’horloge du système pour manipuler l’heure et la date auxquelles le code pense que le test est exécuté.

Donc ma question est:

Existe-t-il un moyen de remplacer ce qui est renvoyé par System.currentTimeMillis ? Par exemple, pour demander à la JVM d’append ou de soustraire automatiquement certains décalages avant de revenir de cette méthode?

Merci d’avance!

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

Cela pourrait presque être automatisé avec la recherche et le remplacement de la version singleton:

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

(etc si nécessaire)

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

tl; dr

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

Oui.

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

Clock In java.time

Nous avons une nouvelle solution au problème d’un remplacement d’horloge enfichable pour faciliter les tests avec de fausses valeurs de date et d’heure. Le package java.time dans Java 8 inclut une classe abstraite java.time.Clock , avec un objective explicite:

pour permettre de twigr des horloges alternatives au besoin

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

Altération de la cadence

Les différentes méthodes tick… produisent des horloges qui incrémentent le moment en cours avec une cadence différente.

L’ Clock par défaut indique une heure mise à jour aussi souvent que des millisecondes en Java 8 et en Java 9, en nanosecondes (selon votre matériel). Vous pouvez demander que le vrai moment actuel soit signalé avec une granularité différente.

  • tickSeconds – Incrémente en secondes entières
  • tickMinutes – Incrémente en minutes entières
  • tick – Incrémente par l’argument Duration passé.

Fausses horloges

Certaines horloges peuvent se trouver, produisant un résultat différent de celui de l’horloge matérielle de l’OS hôte.

  • fixed – Indique un moment unique (non incrémenté) comme le moment actuel.
  • offset – Indique le moment actuel mais décalé par l’argument Duration passé.

Par exemple, verrouillez le premier moment du premier Noël de cette année. en d’autres termes, lorsque le père Noël et son renne font leur premier arrêt . Le premier fuseau horaire semble être le 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 au même moment. Nous avons le premier moment du jour de Noël à Kiritimati , avec UTC affichant une heure de l’ horloge murale de quatorze heures plus tôt, à 10 heures du matin le 24 décembre.

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

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

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

Voir le code en direct sur IdeOne.com .

Temps réel, fuseau horaire différent

Vous pouvez contrôler le fuseau horaire atsortingbué par l’implémentation de l’ 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 ZoneId optionnels ZoneId ou ZoneOffset .

Vous pouvez spécifier que l’UTC soit la zone par défaut.

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

Vous pouvez spécifier un fuseau horaire particulier. Spécifiez un nom de fuseau horaire approprié dans le format de continent/region , tel que America/Montreal , Africa/Casablanca ou Pacific/Auckland . N’utilisez jamais l’abréviation de 3-4 lettres comme EST ou IST car ce ne sont pas de véritables fuseaux horaires, non standardisé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 de la machine virtuelle Java par défaut pour un object d’ Clock particulier.

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

Exécutez ce code pour comparer. Notez qu’ils signalent tous le même moment, le même point sur la chronologie. Ils ne diffèrent que par l’ heure de l’horloge murale ; en d’autres termes, trois manières de dire la même chose, trois manières d’afficher le même moment.

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

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

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

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

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

La classe Instant est toujours définie par UTC. Donc, ces trois utilisations de l’ Clock liées aux zones 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.toSsortingng (): 2016-12-31T20: 52: 39.763Z

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

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

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 d’ Clock . Voyez vous – même dans le code source Java 9 pré-version pour Instant.now .

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

L’ Clock par défaut pour OffsetDateTime.now et ZonedDateTime.now est Clock.systemDefaultZone() . Voir le code source .

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

Le comportement des implémentations par défaut a été modifié entre Java 8 et Java 9. Dans Java 8, le moment actuel est capturé avec une résolution en millisecondes seulement, malgré la capacité des classes à stocker une résolution de nanosecondes . Java 9 apporte une nouvelle implémentation capable de capturer l’instant actuel avec une résolution de nanosecondes, en fonction bien sûr de la capacité de l’horloge de votre ordinateur.


À propos de java.time

Le framework java.time est intégré à Java 8 et versions ultérieures. Ces classes supplantent les anciennes classes de date / heure héritées telles que java.util.Date , Calendar et SimpleDateFormat .

Le projet Joda-Time , maintenant en mode maintenance , conseille la migration vers les classes java.time .

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

Vous pouvez échanger des objects java.time directement avec votre firebase database. 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 les classes java.time?

  • Java SE 8 , Java SE 9 et versions ultérieures
    • Intégré
    • Partie de l’API Java standard avec une implémentation intégrée.
    • Java 9 ajoute quelques fonctionnalités et corrections mineures.
  • Java SE 6 et Java SE 7
    • Une grande partie de la fonctionnalité java.time est transférée vers Java 6 et 7 dans ThreeTen-Backport .
  • Android
    • Versions ultérieures des implémentations de bundles Android des classes java.time.
    • Pour Android antérieur (<26), le projet ThreeTenABP adapte ThreeTen-Backport (mentionné ci-dessus). Voir Comment utiliser ThreeTenABP… .

Le projet ThreeTen-Extra étend java.time avec des classes supplémentaires. Ce projet est un terrain d’essai pour d’éventuels ajouts futurs à java.time. Vous pouvez y trouver des classes utiles telles que Interval , YearWeek , YearQuarter , etc.

Comme l’a dit Jon Skeet :

“Utiliser Joda Time” est presque toujours la meilleure réponse à toute question impliquant “comment puis-je atteindre X avec java.util.Date/Calendar?”

Donc, voilà (en supposant que vous venez de remplacer toutes vos new Date() par de 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 souhaitez importer une bibliothèque dotée d’une interface (voir le commentaire de Jon ci-dessous), vous pouvez simplement utiliser Prevayler’s Clock , qui fournira des implémentations ainsi que l’interface standard. Le pot plein est seulement 96kB, donc il ne devrait pas casser la banque …

Bien que l’utilisation d’un modèle DateFactory semble agréable, il ne couvre pas les bibliothèques que vous ne pouvez pas contrôler – imaginez l’annotation Validation @Past avec l’implémentation reposant sur System.currentTimeMillis (il y en a une).

C’est pourquoi nous utilisons jmockit pour simuler l’heure du système directement:

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

Comme il n’est pas possible d’atteindre la valeur initiale de millis, nous utilisons plutôt nano timer – 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; } 

Il y a un problème documenté: avec HotSpot, le temps redevient normal après un certain nombre d’appels – voici le rapport de problème: http://code.google.com/p/jmockit/issues/detail?id=43

Pour remédier à cela, nous devons activer une optimisation HotSpot spécifique: exécutez JVM avec cet argument -XX:-Inline .

Bien que cela ne soit peut-être pas parfait pour la production, il convient parfaitement aux tests et il est absolument transparent pour les applications, en particulier lorsque DataFactory n’a pas de sens commercial et est introduit uniquement à cause des tests. Il serait bien d’avoir une option JVM intégrée pour exécuter dans des temps différents, tant pis, ce n’est pas possible sans de tels piratages.

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

La classe pratique complète SystemTimeShifter est fournie dans l’article. Class peut être utilisé dans vos tests, ou il peut être utilisé comme la première classe principale avant votre classe principale très facilement afin d’exécuter votre application (ou même un serveur d’applications entier) dans un temps différent. Bien entendu, cela est destiné à des fins de test principalement, pas pour l’environnement de production.

EDIT July 2014: JMockit a beaucoup changé ces derniers temps et vous devez utiliser JMockit 1.0 pour l’utiliser correctement (IIRC). Il est impossible de mettre à niveau vers la nouvelle version où l’interface est complètement différente. Je pensais à mettre en place juste les choses nécessaires, mais comme nous n’avons pas besoin de cette chose dans nos nouveaux projets, je ne développe pas du tout cette chose.

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

Utilisez la programmation orientée aspect (AOP, par exemple AspectJ) pour tisser la classe System afin de renvoyer une valeur prédéfinie que vous pouvez définir dans vos scénarios de test.

Ou bien, intégrez les classes d’application pour redirect l’appel vers System.currentTimeMillis() ou vers new Date() vers une autre classe d’utilitaire.

Tisser des classes de système ( java.lang.* ) Est cependant un peu plus compliqué et vous devrez peut-être effectuer un tissage hors ligne pour rt.jar et utiliser un JDK / rt.jar distinct pour vos tests.

Il s’appelle le tissage binary et il existe également des outils spéciaux pour effectuer le tissage des classes système et contourner certains problèmes (par exemple, le démarrage de la machine virtuelle peut ne pas fonctionner).

Il n’y a pas vraiment de moyen de le faire directement dans la machine virtuelle, mais vous pouvez tout faire pour définir l’heure du système sur la machine de test par programmation. La plupart des systèmes d’exploitation (tous?) Ont des commandes en ligne de commande pour le faire.

Sans refactorisation, pouvez-vous effectuer un test sur une instance de machine virtuelle du système d’exploitation?

À mon avis, seule une solution non invasive peut fonctionner. Surtout si vous avez des librairies externes et une base de code existante, il n’y a pas de moyen fiable de simuler le temps.

JMockit … ne fonctionne que pour un nombre de fois limité

PowerMock & Co … doit simuler les clients vers System.currentTimeMillis (). Encore une option invasive.

À partir de cela, je ne vois que l’approche javaagent ou aop mentionnée comme étant transparente pour l’ensemble du système. Quelqu’un at-il fait cela et pourrait-il indiquer une telle solution?

@jarnbjo: pourriez-vous montrer une partie du code javaagent s’il vous plaît?

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

Voici ce que vous devez faire:

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

Étape 1

Ajoutez un nouvel atsortingbut java.time.Clock à la classe MyService testée et assurez-vous que le nouvel atsortingbut 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

Injecter la nouvelle clock atsortingbut dans la méthode qui appelle une date-heure courante. Par exemple, dans mon cas, j’ai dû vérifier si une date stockée dans dataase était antérieure à LocalDateTime.now() , que j’ai remplacée 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 de test, créez un object d’horloge fictif et injectez-le dans l’instance de la classe testée juste avant d’appeler la méthode testée doExecute() , puis réinitialisez-la immédiatement 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 correctement réinitialisée à la date actuelle avec initDefaultClock() .

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

Il suffit de définir la variable d’environnement avec l’heure à laquelle vous souhaitez simuler votre application java et de l’exécuter avec 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 seraient gelées. Il nécessite la twig principale de libfaketime au moment de l’écriture.

Si vous souhaitez modifier l’heure d’un service géré systemd, ajoutez simplement ce qui suit à vos remplacements de fichiers d’unité, par exemple pour elasticsearch, il s’agirait de /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

Si vous voulez simuler la méthode avec l’argument System.currentTimeMillis() , vous pouvez passer anyLong() de la classe Matchers en tant qu’argument.

PS Je peux exécuter mon test avec succès en utilisant l’astuce ci-dessus et juste pour partager plus de détails sur mon test avec les frameworks PowerMock et Mockito.