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

129

Existe-t-il un moyen, que ce soit dans le 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 contexte:

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 (c'est-à-dire le 1er du mois, le 1er de l'année, etc.)

Malheureusement, une grande partie du code hérité appelle des fonctions telles que new Date()ou Calendar.getInstance(), qui finissent par appeler System.currentTimeMillis.

À des fins de test, pour le moment, nous sommes obligés de 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.

Ma question est donc:

Existe-t-il un moyen de remplacer ce qui est retourné 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!

Mike Clark
la source
1
Je ne sais pas si c'est plus pertinent, mais il existe une autre méthode pour y parvenir avec AspectJ, voir ma réponse à: stackoverflow.com/questions/18239859/...
Nándor Előd Fekete
@ NándorElődFekete la solution dans le lien est intéressante, cependant elle nécessite de recompiler le code. Je me demande si l'affiche originale a la capacité de se recompiler, étant donné qu'il prétend avoir affaire à du code hérité.
cleberz
1
@cleberz Une des propriétés intéressantes d'AspectJ est qu'il fonctionne directement sur le bytecode, il n'a donc pas besoin du code source original pour fonctionner.
Nándor Előd Fekete
@ NándorElődFekete merci beaucoup pour cet indice. Je n'étais pas au courant de l'instrumentation de niveau bytecode aspectJ (en particulier l'instrumentation des classes JDK). Cela m'a pris un certain temps, mais j'ai pu comprendre que je devais à la fois un tissage au moment de la compilation du rt.jar ainsi qu'un tissage au moment du chargement des classes non-jdk afin de répondre à mes besoins (surcharger System. currentTimeMillis () et System.nanoTime ()).
cleberz
@cleberz J'ai une autre réponse pour tisser des classes JRE , vous pouvez également la vérifier.
Nándor Előd Fekete

Réponses:

115

Je recommande fortement qu'au lieu de jouer avec l'horloge 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'injection de dépendances, 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().
  • Remplacer new Date()parClock.getInstance().newDate()
  • Remplacer System.currentTimeMillis()parClock.getInstance().currentTimeMillis()

(etc. au besoin)

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

Jon Skeet
la source
50
Encombrer votre code en faisant abstraction ou en enveloppant toutes les API potentielles pour augmenter la testabilité n'est pas à mon humble avis une très bonne idée. Même si vous pouvez effectuer la refactorisation une fois avec une simple recherche et remplacement, le code devient beaucoup plus difficile à lire, à comprendre et à maintenir. Plusieurs frameworks fictifs devraient pouvoir modifier le comportement de System.currentTimeMillis, sinon, utiliser AOP ou une instrumentalisation faite soi-même est le meilleur choix.
jarnbjo
21
@jarnbjo: Eh bien, vous êtes les bienvenus à votre avis, bien sûr - mais c'est une technique assez bien établie de l'OMI, et je l'ai certainement utilisée à bon escient. Non seulement cela améliore la testabilité, mais cela rend également votre dépendance à l'heure du système explicite, ce qui peut être utile pour obtenir une vue d'ensemble du code.
Jon Skeet
4
@ virgo47: Je pense que nous devrons accepter de ne pas être d'accord. Je ne vois pas "énoncer explicitement vos dépendances" comme polluant le code - je le vois comme une manière naturelle de rendre le code plus clair. Je ne vois pas non plus le fait de cacher ces dépendances via des appels statiques inutilement comme "parfaitement acceptable".
Jon Skeet
26
MISE À JOUR Le nouveau package java.time intégré à Java 8 comprend une java.time.Clockclasse «pour permettre à d'autres horloges d'être connectées au besoin».
Basil Bourque
7
Cette réponse implique que DI est tout bon. Personnellement, je n'ai pas encore vu d'application dans le monde réel qui en fasse bon usage. Au lieu de cela, nous voyons une profusion d'interfaces séparées inutiles, d '"objets" sans état, d' "objets" de données uniquement et de classes à faible cohésion. IMO, la simplicité et la conception orientée objet (avec de vrais objets pleins d'état) est un meilleur choix. En ce qui concerne les staticméthodes, assurez-vous qu'elles ne sont pas OO, mais injecter une dépendance sans état dont l'instance ne fonctionne sur aucun état d'instance n'est pas vraiment mieux; c'est juste une manière élégante de déguiser ce qui est effectivement un comportement «statique» de toute façon.
Rogério
78

tl; dr

Existe-t-il un moyen, dans le code ou avec des arguments JVM, de remplacer l'heure actuelle, telle que présentée via System.currentTimeMillis, autre que de modifier 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 Dans 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-heure. Le package java.time de Java 8 comprend une classe abstraite java.time.Clock, avec un objectif explicite:

pour permettre le branchement d'horloges alternatives 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 pendant les tests.

Cadence modifiée

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

La valeur par défaut Clocksignale une heure mise à jour aussi souvent que des millisecondes en Java 8 et en Java 9 aussi bien que des nanosecondes (selon votre matériel). Vous pouvez demander que le moment réel 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 inchangé (non incrémentiel) comme moment actuel.
  • offset- Rapporte le moment actuel mais décalé par l' Durationargument 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 ses rennes font leur premier arrêt . Le fuseau horaire le plus ancien 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 renvoyer le même moment. Nous obtenons le premier moment du jour de Noël à Kiritimati , avec UTC indiquant une horloge murale de quatorze heures plus tôt, 10 heures du matin à la date précédente 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 le code en direct sur IdeOne.com .

Heure vraie, fuseau horaire différent

Vous pouvez contrôler le fuseau horaire attribué par l' Clockimplémentation. 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 ZoneIdou les ZoneOffsetarguments.

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. Indiquez un nom de fuseau horaire approprié dans le format continent/region, tels que America/Montreal, Africa/Casablancaou Pacific/Auckland. N'utilisez jamais l'abréviation de 3 à 4 lettres telle que ESTou ISTcar ce ne sont pas de vrais fuseaux horaires, non standardisés et même pas uniques (!).

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

Vous pouvez spécifier que le fuseau horaire par défaut actuel de la machine virtuelle Java doit être celui par défaut pour un Clockobjet 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 par l' 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 actuelle de la JVM sur l'ordinateur qui exécutait 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 Instantclasse est toujours en UTC par définition. Donc, ces trois Clockusages 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.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.nowest celle renvoyée par Clock.systemUTC(). Il s'agit de l'implémentation utilisée lorsque vous ne spécifiez pas de fichier Clock. Voyez par vous -Instant.now même dans le code source de Java 9 pré-version pour .

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

La valeur Clockpar défaut pour OffsetDateTime.nowet ZonedDateTime.nowest Clock.systemDefaultZone(). Voir le 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 en millisecondes seulement 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 framework java.time est intégré à Java 8 et versions ultérieures. Ces classes supplantent les anciens gênants hérités des classes date-heure tels que java.util.Date, Calendar, et SimpleDateFormat.

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

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

Vous pouvez échanger des objets java.time 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 java.sql.*classes. Hibernate 5 et JPA 2.2 prennent en charge java.time .

Où obtenir les classes java.time?

Basil Bourque
la source
c'est la bonne réponse
bharal
42

Comme l'a dit Jon Skeet :

"utiliser Joda Time" est presque toujours la meilleure réponse à toute question concernant "comment obtenir X avec java.util.Date/Calendar?"

Alors voilà (en supposant que vous venez de tout remplacer 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 qui a 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 ne pèse que 96 Ko, il ne devrait donc pas casser la banque ...

Stephen
la source
13
Non, je ne recommanderais pas cela - cela ne vous déplace pas vers une horloge véritablement remplaçable; il vous permet d'utiliser la statique tout au long. Utiliser Joda Time est bien sûr une bonne idée, mais je voudrais toujours utiliser une interface Clock ou similaire.
Jon Skeet
@Jon: Vrai - pour les tests, ça va, mais il vaut mieux avoir une interface.
Stephen le
5
@Jon: C'est un moyen fluide et parfait à mon humble avis si vous souhaitez tester du code dépendant du temps qui utilise déjà JodaTime. Essayer de réinventer la roue en introduisant encore une autre couche / structure d'abstraction n'est pas la voie à suivre, si tout a déjà été résolu pour vous avec JodaTime.
Stefan Haberl
2
@JonSkeet: Ce n'est pas ce que Mike demandait à l'origine. Il voulait un outil pour tester le code dépendant du temps et non une horloge remplaçable à part entière dans le code de production. Je préfère garder mon architecture aussi complexe que nécessaire mais aussi simple que possible. Introduire ici une couche supplémentaire d'abstraction (c'est-à-dire l'interface) n'est tout simplement pas nécessaire
Stefan Haberl
2
@StefanHaberl: Il y a beaucoup de choses qui ne sont pas strictement "nécessaires" - mais ce que je suggère vraiment, c'est de rendre explicite une dépendance déjà présente et plus facilement testable isolément. Simuler le temps système avec la statique a tous les problèmes normaux de la statique - c'est un état global sans raison valable . Mike demandait un moyen d'écrire du code testable utilisant la date et l'heure actuelles. Je crois toujours que ma suggestion est beaucoup plus propre (et rend les dépendances plus claires) que de truquer efficacement une statique.
Jon Skeet
15

Bien que l'utilisation d'un modèle DateFactory semble agréable, cela 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 y en a une).

C'est pourquoi nous utilisons jmockit pour simuler directement l'heure du 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);

Comme il n'est pas possible d'obtenir la valeur non simulée d'origine de millis, nous utilisons à la place une nano minuterie - 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 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 HotSpot spécifique - exécutez JVM avec cet argument -XX:-Inline.

Bien que cela ne soit peut-être pas parfait pour la production, cela convient parfaitement aux tests et est absolument transparent pour l'application, en particulier lorsque DataFactory n'a pas de sens commercial et est introduit uniquement en raison de tests. Ce serait bien d'avoir une option JVM intégrée pour fonctionner à un moment différent, dommage que ce ne soit pas possible sans des 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/

La classe pratique complète SystemTimeShifter est fournie dans l'article. La classe peut être utilisée dans vos tests, ou elle peut être utilisée comme la première classe principale avant votre vraie classe principale très facilement afin d'exécuter votre application (ou même le serveur d'applications entier) à un moment différent. Bien sûr, cela est principalement destiné à des fins de test, pas à 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). Impossible de passer à la dernière version où l'interface est complètement différente. Je pensais à intégrer uniquement les éléments nécessaires, mais comme nous n'avons pas besoin de ce truc dans nos nouveaux projets, je ne développe pas du tout ce truc.

virgo47
la source
1
Le fait que vous modifiez l'heure pour chaque classe va dans les deux sens ... par exemple, si vous vous moquez de nanoTime au lieu de currentTimeMillis, vous devrez faire très attention à ne pas casser java.util.concurrent. En règle générale, si une bibliothèque tierce est difficile à utiliser dans les tests, vous ne devriez pas vous moquer des entrées de la bibliothèque: vous devez vous moquer de la bibliothèque elle-même.
Dan Berindei
1
Nous utilisons cette technique pour des tests où nous ne nous moquons de rien d'autre. C'est un test d'intégration et nous testons l'ensemble de la pile pour savoir si elle fait ce qu'elle devrait quand une activité se produit la première fois de l'année, etc. Bon point à propos de nanoTime, heureusement nous n'avons pas besoin de nous moquer de celle-là car elle n'a pas d'horloge murale sens du tout. J'adorerais voir Java avec des fonctionnalités qui changent dans le temps car j'en avais déjà besoin plus d'une fois et jouer avec l'heure système à cause de cela est très malchanceux.
virgo47
7

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

René
la source
J'ai essayé d'utiliser Powermock, et si je le comprenais correctement, cela ne se moquerait pas vraiment du côté de l'appelé. Au lieu de cela, il trouve tous les appelants et applique la simulation. Cela peut prendre beaucoup de temps car vous devez le laisser vérifier CHAQUE classe dans votre chemin de classe si vous voulez être vraiment sûr que votre application fonctionne en temps décalé. Corrigez-moi si je me trompe sur la façon dont Powermock se moque.
virgo47
1
@ virgo47 Non, vous avez mal compris. PowerMock ne "vérifiera jamais chaque classe de votre chemin de classe"; il ne vérifiera que les classes que vous lui demandez spécifiquement de vérifier (via l' @PrepareForTestannotation sur la classe de test).
Rogério
1
@ Rogério - c'est exactement ce que je comprends - j'ai dit "il faut le laisser ...". C'est-à-dire que si vous vous attendez à appeler System.currentTimeMillisn'importe où sur votre chemin de classe (toute bibliothèque), vous devez vérifier chaque classe. C'est ce que je voulais dire, rien d'autre. Le fait est que vous vous moquez de ce comportement du côté de l'appelant ("vous dites spécifiquement"). C'est correct pour les tests simples mais pas pour les tests où vous ne pouvez pas être sûr de ce qui appelle cette méthode d'où (par exemple des tests de composants plus complexes avec des bibliothèques impliquées). Cela ne signifie pas du tout que Powermock a tort, cela signifie simplement que vous ne pouvez pas l'utiliser pour ce type de test.
virgo47
1
@ virgo47 Oui, je vois ce que tu veux dire. Je pensais que vous vouliez dire que PowerMock devrait examiner automatiquement chaque classe dans le classpath, ce qui serait évidemment absurde. En pratique, cependant, pour les tests unitaires, nous savons généralement quelle (s) classe (s) lit l'heure, donc ce n'est pas un problème de la spécifier; pour les tests d'intégration, en effet, l'exigence dans PowerMock d'avoir toutes les classes d'utilisation spécifiées peut être un problème.
Rogério
6

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

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

Classes de système de tissage (java.lang.* ) est cependant un peu plus délicat et vous devrez peut-être effectuer un tissage hors ligne pour rt.jar et utiliser un JDK / rt.jar séparé pour vos tests.

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

mhaller
la source
Cela fonctionne, bien sûr, mais c'est beaucoup plus facile à faire avec un outil de simulation qu'avec un outil AOP; ce dernier type d'outil est tout simplement trop générique pour l'objectif en question.
Rogério
3

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

Jérémy Raymond
la source
Par exemple, sous Windows, les commandes dateet time. Sous Linux, la datecommande.
Jeremy Raymond
Pourquoi ne serait-il pas possible de faire cela avec l'AOP ou l'instrumentalisation (cela a-t-il été fait)?
jarnbjo
3

Un moyen efficace 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:

Que faut-il faire dans la classe testée

Étape 1

Ajoutez un nouvel java.time.Clockattribut à la classe testée MyServiceet assurez-vous que le nouvel attribut sera correctement initialisé 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 clockdans 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 était antérieure LocalDateTime.now(), que j'ai remplacée 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();
    }
  }
  // (...) 
}

Que faut-il faire dans la classe de test

Étape 3

Dans la classe de test, créez un objet 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-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' myServiceinstance et utilisée dans l'instruction de comparaison, puis a été correctement réinitialisée à la date actuelle avec initDefaultClock().

KiriSakow
la source
2

À 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 simuler le temps.

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

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

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

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

user1477398
la source
3
jmockit solution fonctionnant un nombre illimité de fois avec un petit -XX: -Hack en ligne (sur Sun's HotSpot): virgo47.wordpress.com/2012/06/22/changing-system-time-in-java Une classe pratique complète SystemTimeShifter est fournie dans la poste. La classe peut être utilisée dans vos tests, ou elle peut être utilisée comme la première classe principale avant votre vraie classe principale très facilement afin d'exécuter votre application (ou même le serveur d'applications entier) à un moment différent. Bien sûr, cela est principalement destiné à des fins de test, pas à l'environnement de production.
virgo47
2

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

Définissez simplement la variable d'environnement avec l'heure avec laquelle vous souhaitez simuler 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 sinon se figeraient. Il nécessite la branche principale de libfaketime au moment de l'écriture.

Si vous souhaitez modifier l'heure d'un service géré par 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

faxmodem
la source
-1

Si vous voulez vous moquer de la méthode ayant un System.currentTimeMillis()argument, vous pouvez passeranyLong() classe Matchers comme argument.

PS Je suis capable 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.

Paresh Mayani
la source