Outrepasser Le Système Java.currentTimeMillis pour tester le code sensible au temps

y a-t-il un moyen, en code ou avec des arguments JVM, de modifier l'heure actuelle, telle qu'elle est présentée par System.currentTimeMillis , autrement que de changer manuellement l'horloge du système sur la machine hôte?

un petit fond:

nous avons un système qui exécute un certain nombre d'emplois de comptabilité qui tournent une grande partie de leur logique autour de la date actuelle (ie 1er du mois, 1er de l'année, etc)

malheureusement, une grande partie du code d'héritage les fonctions d'appel telles que new Date() ou Calendar.getInstance() , qui finissent toutes les deux par appeler System.currentTimeMillis .

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

donc ma question Est:

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

Merci d'avance!

98
demandé sur Ashish Kumar 2010-01-04 22:39:53

12 réponses

i fortement recommande qu'au lieu de jouer avec l'horloge du système, vous mordez la balle et refactor que le code d'héritage d'utiliser une horloge remplaçable. idéalement cela devrait être fait avec l'injection de dépendance, mais même si vous utilisez un singleton remplaçable, vous gagneriez testabilité.

cela pourrait presque être automatisé avec search et remplacer pour la version singleton:

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

(etc, comme l'exige)

une fois que vous avez fait ce premier pas, vous pouvez remplacer le singleton par DI un peu à la fois.

91
répondu Jon Skeet 2010-01-04 19:43:46

tl; dr

est-il possible, en code ou avec des arguments JVM, de surcharger l'heure actuelle, telle qu'elle est présentée via le système.currentTimeMillis, autre que le changement manuel de l'horloge du 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 du remplacement de l'horloge test avec fausses valeurs date-heure. Le java.package dans Java 8 comprend une classe abstraite java.time.Clock , avec un but explicite:

pour permettre de brancher d'autres horloges au besoin

vous pouvez brancher votre propre implémentation de Clock , mais vous pouvez probablement en trouver une déjà pour répondre à vos besoins. Pour votre commodité, java.le temps inclut des méthodes statiques pour produire des implémentations spéciales. Ces implémentations alternatives peuvent être précieuses pendant les tests.

cadence modifiée

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

par défaut Clock rapports une fois mis à jour aussi fréquemment que millisecondes en Java 8 et en Java 9 aussi bien que nanosecondes (selon votre matériel). Vous pouvez demander que le vrai moment actuel soit rapporté avec une granularité différente.

fausses horloges

Certaines horloges peuvent mentir, produire un résultat différent de celui de l'OS hôte’ horloge matérielle.

  • fixed - signale un seul moment immuable (Non incrémentant) comme le moment actuel.
  • offset - Rapporte le moment actuel mais décalé par l'argument passé Duration .

par exemple, verrouiller dans le premier moment de la Noël plus tôt cette année. en d'autres termes, quand Santa et son renne font leur premier arrêt . Le premier fuseau horaire semble aujourd'hui ê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 ce fixe spécial horloge 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 );

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

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

Voir code en direct en IdeOne.com .

vrai temps, fuseau horaire différent

vous pouvez contrôler quel fuseau horaire est assigné par l'implémentation Clock . Cela peut être utile dans certains tests. Mais je ne le recommande pas dans le code de production, où vous devez toujours spécifier explicitement l'option ZoneId ou ZoneOffset " argument.

vous pouvez spécifier que UTC soit la zone par défaut.

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

vous pouvez spécifier n'importe quel fuseau horaire particulier. Préciser un nom du fuseau horaire approprié dans le format 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 ils sont pas vrai fuseaux horaires, non standardisé, et même pas unique(!).

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

vous pouvez spécifier que le fuseau horaire par défaut de la JVM doit être le fuseau horaire par défaut pour un objet particulier Clock .

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

exécutez ce code pour comparer. Notez qu'ils rapportent tous au même moment, le même point sur la timeline. 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 de courant 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[America/Montreal]

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

la classe Instant est toujours en TUC par définition. Donc ces trois usages Clock liés à la zone 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 retournée par Clock.systemUTC() . C'est l'implémentation utilisée lorsque vous ne spécifiez pas un Clock . Voir pour vous-même dans pré-release Java 9 code source pour Instant.now .

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

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. En Java 8, le moment actuel est capturé avec une résolution seulement 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 présent avec un résolution de nanosecondes-en fonction, bien sûr, de la capacité de votre horloge matérielle informatique.


à Propos de de java.heure

Le de java.time framework est construit dans Java 8 et plus tard. Ces classes remplacent les anciennes héritées classes de date-heure telles que java.util.Date , Calendar , & SimpleDateFormat .

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

pour en savoir plus, voir le Oracle Tutorial . Et rechercher le débordement de la pile pour de nombreux exemples et des explications. La spécification est JSR 310 .

vous pouvez échanger java.temps objets directement avec votre base de données. Utilisez un JDBC driver conforme à JDBC 4.2 ou plus tard. Pas besoin de Chaînes, pas besoin de classes java.sql.* .

où obtenir le java.les classes de temps?

ThreeTen-Extra le projet étend java.temps avec des cours supplémentaires. Ce projet est un terrain d'expérimentation pour d'éventuelles futures additions à java.temps. Vous pouvez trouver ici quelques cours utiles tels que: Interval , YearWeek , YearQuarter , et plus .

47
répondu Basil Bourque 2018-03-16 20:27:23

comme dit par Jon Skeet :

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

donc voici (présumant 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 juste utilisez Prevayler's Clock , qui fournira des implémentations ainsi que l'interface standard. Le bocal plein est seulement 96kB, donc il ne devrait pas briser la banque...

40
répondu Stephen 2010-01-04 21:39:10

bien que l'utilisation d'un modèle de répertoire de données semble agréable, il ne couvre pas les bibliothèques que vous ne pouvez pas contrôler - imaginez l'annotation de Validation @Past avec la mise en œuvre en se basant sur le système.currentTimeMillis (il y a Tel).

C'est pourquoi nous utilisons jmockit pour simuler directement le temps 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 originale non garnie 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;
}

il y a un problème documenté, qu'avec HotSpot le temps revient à la normale après un certain nombre d'appels - voici le rapport de la question: http://code.google.com/p/jmockit/issues/detail?id=43

pour surmonter cela, nous devons tourner sur une optimisation HotSpot spécifique - exécuter JVM avec cet argument -XX:-Inline .

bien que cela ne soit pas parfait pour la production, il c'est très bien pour les tests et c'est absolument transparent pour l'application, surtout quand DataFactory n'a pas de sens commercial et n'est introduit que pour des tests. Ce serait bien d'avoir l'option JVM intégrée pour tourner à des heures différentes, dommage que ce ne soit pas possible sans des hacks comme celui-ci.

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

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

EDIT juillet 2014: JMockit a beaucoup changé dernièrement et vous êtes obligé d'utiliser JMockit 1.0 Pour l'utiliser correctement (IIRC). Ne peut certainement pas mise à niveau vers la version la plus récente où l'interface est complètement différente. Je pensais juste à la distribution des trucs nécessaires, mais comme nous n'avons pas besoin de cette chose dans nos nouveaux projets, je ne développerai pas cette chose du tout.

12
répondu virgo47 2014-07-14 09:52:34

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

7
répondu ReneS 2016-05-11 12:51:43

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

ou tisser les classes d'application pour rediriger l'appel à System.currentTimeMillis() ou à new Date() vers une autre classe d'utilité de votre propre.

les classes de système de tissage ( java.lang.* ) est cependant un peu plus délicat et vous pourriez avoir besoin d'effectuer le tissage hors ligne pour rt.jar et utiliser un séparer JDK/rt.jar pour tes tests.

on l'appelle tissage binaire et il y a aussi des outils pour effectuer le tissage des classes de système et contourner certains problèmes avec cela (par exemple bootstrapping la VM peut ne pas fonctionner)

5
répondu mhaller 2010-01-04 19:49:01

il n'y a vraiment pas moyen de faire cela directement dans la VM, mais vous pourriez tous quelque chose pour programmer le temps système sur la machine de test. La plupart (toutes?) OS ont des commandes en ligne de commande pour le faire.

3
répondu Jeremy Raymond 2010-01-04 19:43:30

sans faire de re-factoring, pouvez-vous tester l'exécution dans une instance de machine virtuelle OS?

3
répondu Jé Queue 2010-01-04 21:18:15

à mon avis, seule une solution non invasive peut fonctionner. Surtout si vous avez des libs externes et une grande base de code legacy, il n'y a pas de moyen fiable de se débarrasser du temps.

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

PowerMock & Co ...doit se moquer du clients au système.currentTimeMillis (). Encore une fois une option invasive.

de ceci Je ne vois que le mentionné javaagent ou aop approche transparente à l'ensemble du système. Personne n'a fait cela et il peut pointer vers une telle solution?

@jarnbjo: pouvez-vous montrer le code javaagent s'il vous plaît?

2
répondu user1477398 2012-06-23 22:46:32

une façon de travailler pour outrepasser le temps système actuel pour des fins de test de 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

Ajouter un nouvel attribut java.time.Clock à la classe testée MyService et s'assurer que le nouvel attribut sera initialisé correctement à valeurs par défaut avec un bloc 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
  }
  // (...)
}

l'Étape 2

injecte le nouvel attribut clock dans la méthode qui demande une date-heure courante. Par exemple, dans mon cas, j'ai dû vérifier si une date stockée dans dataase se produisait 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 d'essai

Étape 3

dans la classe test, créez un objet d'horloge simulée 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 immédiatement 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 de débogage et vous verrez que la date du 3 février 2017 a été correctement injecté dans myService instance et utilisé dans l'instruction de comparaison, puis a été correctement réinitialisé à la date actuelle avec initDefaultClock() .

2
répondu KiriSakow 2017-08-23 07:22:55

si vous utilisez Linux, vous pouvez utiliser la branche principale de libfaketime, ou au moment de tester commit 4ce2835 .

définissez simplement la variable d'environnement avec le temps avec lequel vous souhaitez vous moquer de votre application java et lancez-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 seconde variable d'environnement est primordiale pour les applications java, qui autrement se figeraient. Il nécessite la branche principale de libfaketime à le temps de l'écriture.

si vous souhaitez modifier l'heure d'un service géré par systemd, il vous suffit d'ajouter ce qui suit à vos dérogations 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
répondu 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 la classe Matchers comme un argument.

P.S. 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 PowerMock et Mockito frameworks.

-1
répondu Paresh Mayani 2015-08-12 10:12:31