web-dev-qa-db-fra.com

Remplacer Java System.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, Autrement qu'en modifiant 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 d’emplois comptables dont la logique s’articule autour de la date du jour (c.-à-d. Le 1er du mois, le 1er de l’année, etc.).

Malheureusement, un grand nombre d'appels de code hérités, tels que new Date() ou Calendar.getInstance(), s'appellent tous les deux jusqu'à System.currentTimeMillis.

À des fins de test, pour l'instant, nous ne pouvons pas mettre à jour manuellement l'horloge système pour manipuler l'heure et la date auxquelles le code pense que le test est en cours d'exécution.

Donc ma question est:

Y a-t-il un moyen de remplacer ce qui est retourné par System.currentTimeMillis? Par exemple, pour indiquer à la machine virtuelle Java d’ajouter ou de soustraire automatiquement un décalage avant de revenir de cette méthode?

Merci d'avance!

116
Mike Clark

Je fortement recommande au lieu de jouer avec l'horloge système, de mordre la balle et de refactoriser ce code hérité pour utiliser une horloge remplaçable. Idéalement cela devrait être fait avec une 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:

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

(etc au besoin)

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

106
Jon Skeet

tl; dr

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, autrement qu'en modifiant 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.time

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

pour permettre de brancher des horloges de remplacement au besoin

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

Cadence altérée

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

La valeur par défaut Clock indique une heure mise à jour aussi souvent que millisecondes dans Java 8 et dans Java 9) as nanosecondes (en fonction de votre matériel). Vous pouvez demander que le 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 - Indique un moment unique non-changeant (non incrémenté) en tant que moment actuel.
  • offset - Indique le moment actuel mais décalé de l'argument passé Duration .

Par exemple, verrouillez le premier moment du premier Noël de cette année. autrement dit, quand le père Noël et ses rennes font leur premier arrêt . Le premier fuseau horaire actuel 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 revenir au même moment. Nous avons le premier moment de Noël en Kiritimati , avec UTC indiquant un heure de l'horloge murale de quatorze heures plus tôt, à 10 heures du matin, la date antérieure du 24 décembre.

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 [Pacifique/Kiritimati]

Voir code live sur 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 le recommande pas dans le code de production, où vous devriez toujours spécifier explicitement les arguments optionnels ZoneId ou ZoneOffset.

Vous pouvez spécifier que 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 correct au format continent/region, Tel que America/Montreal , Africa/Casablanca , ou Pacific/Auckland. N'utilisez jamais les abréviations de 3 ou 4 lettres telles que EST ou IST, car elles sont pas de vrais fuseaux horaires, ni normalisés ni uniques. (!).

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

Vous pouvez spécifier que le fuseau horaire actuel par défaut de la machine virtuelle Java doit être celui par défaut pour un objet 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 timeline. Ils ne diffèrent que par heure de l'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 de la machine virtuelle Java sur l'ordinateur qui a exécuté ce code.

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

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

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

La classe Instant est toujours en UTC par définition. Donc, ces trois utilisations de 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.toString (): 2016-12-31T20: 52: 39.763Z

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

instantClockSystemDefaultZone.toString (): 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() . Ceci est l'implémentation utilisée lorsque vous ne spécifiez pas un Clock. Voyez vous-même dans version préliminaire Java 9 pour 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, l'instant présent 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 l'horloge matérielle de votre ordinateur.


À propos de Java.time

Le cadre Java.time est intégré à Java 8 et versions ultérieures. Ces classes supplantent l'ancien problème - legacy classes date-heure telles que Java.util.Date , Calendar , & SimpleDateFormat .

Le projet Joda-Time , actuellement en mode de maintenance , conseille de migrer vers le Java.time = cours.

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

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

Où obtenir les classes Java.time?

Le projet ThreeTen-Extra étend Java.time avec des classes supplémentaires. Ce projet est un terrain d’essai pour d’éventuels ajouts à Java.time. Vous pouvez trouver ici quelques classes utiles telles que Interval , YearWeek , YearQuarter , et plus .

62
Basil Bourque

Comme dit par Jon Skeet :

"use Joda Time" est presque toujours la meilleure réponse à toute question concernant "comment puis-je atteindre X avec Java.util.Date/Calendar?"

Donc, voici (supposons 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 souhaitez importer une bibliothèque ayant 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 complet ne fait que 96 Ko, il ne faut donc pas casser sa tirelire ...

42
Stephen

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 System.currentTimeMillis (il en existe 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'obtenir la valeur non modifiée de millis d'origine, nous utilisons à la place un minuteur nano - ceci 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 existe un problème documenté: avec HotSpot, l'heure est revenue à la normale après plusieurs appels. Voici le rapport de problème: http://code.google.com/p/jmockit/issues/detail?id= 4

Pour résoudre ce problème, nous devons activer une optimisation HotSpot spécifique - exécutez JVM avec cet argument -XX:-Inline.

Bien que cela puisse ne pas être parfait pour la production, cela convient parfaitement pour les tests et il est absolument transparent pour l'application, en particulier lorsque DataFactory n'a pas de sens commercial et est introduit uniquement à cause des tests. Il serait bien d’avoir l’option JVM intégrée pour s’exécuter à une heure différente, dommage que ce ne soit pas possible sans ce genre de piratage.

L'article complet est dans mon article de blog ici: http://virgo47.wordpress.com/2012/06/22/changing-system-time-in-Java/

La classe maniable complète SystemTimeShifter est fournie dans le post. Class peut être utilisé dans vos tests ou comme première classe principale avant votre vraie classe principale afin de pouvoir exécuter votre application (ou même un serveur d'applications entier) à un moment différent. Bien sûr, ceci est destiné principalement à des fins de test, pas pour un environnement de production.

EDIT Juillet 2014: JMockit a beaucoup changé récemment et vous êtes obligé d'utiliser JMockit 1.0 pour l'utiliser correctement (IIRC). Ne peut certainement pas passer à la dernière version où l'interface est complètement différente. Je pensais à la mise en ligne des éléments nécessaires, mais comme nous n'en avons pas besoin dans nos nouveaux projets, je ne les développe pas du tout.

15
virgo47

Powermock fonctionne très bien. Je viens de l'utiliser pour me moquer de System.currentTimeMillis().

7
ReneS

Utilisez la programmation par 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 cas de test.

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

Classes de système de tissage (Java.lang.*) est toutefois 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.

Cela s'appelle le tissage binaire et il existe aussi des outils) spéciaux (= outils permettant de tisser des classes système et de contourner certains problèmes (par exemple, bootstrapping le VM peut ne pas fonctionner)

6
mhaller

Il n’existe vraiment aucun moyen de le faire directement dans la machine virtuelle, mais vous pouvez également définir l’heure système par programme sur la machine de test. La plupart (tous?) OS ont des commandes en ligne de commande pour le faire.

3
Jeremy Raymond

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 MyService 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, je devais vérifier si une date stockée dans la base de données était antérieure à LocalDateTime.now(), que j'ai remplacée par LocalDateTime.now(clock), comme suit:

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 objet d'horloge factice 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 suit:

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 du jour avec initDefaultClock(). .

3
KiriSakow

A mon avis, seule une solution non invasive peut fonctionner. Surtout si vous avez des bibliothèques externes et une base de code héritée importante, il n’existe aucun moyen fiable de se moquer du temps.

JMockit ... ne fonctionne que pour un nombre restreint de times

PowerMock & Co ... doit se moquer de clients à System.currentTimeMillis (). Encore une fois une option invasive.

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

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

2
user1477398

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

Définissez simplement la variable d'environnement avec l'heure à laquelle vous souhaitez vous moquer de votre Java avec, et exécutez-la à l'aide de 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 sinon gèleraient. Elle nécessite la branche maître de libfaketime au moment de l'écriture.

Si vous souhaitez modifier l'heure d'un service géré par systemd, ajoutez simplement les éléments suivants à 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
faxmodem