Craftsman Recipes – Simuler le temps

Ce nouvel article de la série Craftsman Recipes se penche sur une technique très simple mais que l’on rencontre sur peu de projets : la simulation de l’instant présent.

Nos applications doivent couramment prendre des décisions dépendant de l’instant auquel elles s’exécutent (date, heure…). Il n’est donc pas rare d’avoir du code effectuant des tests sur la base d’une donnée temporelle retournée par Date, Calendar ou encore System.currentTimeMillis(), par exemple :

Calendar now = Calendar.getInstance();
// si après 12h40
if (now.get(Calendar.HOUR) >= 12 && now.get(Calendar.MINUTE) > 30) // alors...

ou encore :

// si événement passé
if (someDate.before(new Date())) // alors...

Ce type de code pose plusieurs problèmes :

  • La lisibilité : les APIs de date Java ne sont pas réputées pour leur côté pratique et leur expressivité (de même pour JavaScript). En attendant Java 8, on se tournera vers Joda Time pour remédier à cela.
  • La testabilité : ce type de code est absolument intestable sans avoir la main sur date du système.

Cet article s’intéresse au deuxième cas et décrit une solution simple pour remédier au problème.

Simuler le temps

Quand une application doit appeler un service externe, on choisit en général l’une des solutions suivantes pour la tester :

  • remplacer le système externe par un autre sur lequel on a la main, via la configuration de l’application (putôt pour des tests d’intégration) ;
  • remplacer le code appelant le système par un code différent, via de l’injection de dépendances (plutôt pour des tests unitaires).

Il est plus rare de simuler l’accès au temps. Et pourtant, il s’agit là aussi d’un service externe sur lequel nous n’avons aucun contrôle. Dès lors, pourquoi ne pas appliquer la même stratégie ?

Voici un exemple de code tout simple permettant de simuler le temps :

public interface Clock {

 // on peut utiliser les APIs Java...
 long currentTimeMillis();

 Date currentDate();

 // etc.

 // ... ou en profiter pour retourner directement des objets d'une API plus sympa
 DateTime now();
}

// utilisée en production
public class SystemClock implements Clock {

 public long currentTimeMillis() {
  return System.currentTimeMillis();
 }

 public Date currentDate() {
  return new Date();
 }

 public DateTime now() {
  return DateTime.now();
 }
}

// utilisée pour les tests
public class FixedClock implements Clock {

 private DateTime now;


 public long currentTimeMillis() {
  return now.getMillis();
 }

 public Date currentDate() {
  return now.toDate();
 }

 public DateTime now() {
  return now;
 }

 public void setCurrentDateTime(DateTime newDateTime) {
  this.now = newDateTime;
 }
}

Rien de bien sorcier. Dès lors, tout nous est possible : utiliser nos APIs préférées, retourner des valeurs différentes en fonction de l’appel, créer une implémentation configurable depuis l’extérieur de l’application pour les tests d’intégration, etc…
Tout code désirant accéder à l’instant présent utilisera l’interface Clock, sans savoir qu’elle implémentation lui est donnée. Reste à récupérer l’implémentation en question…

Récupération de l’horloge

Voici quelques approches qui s’offrent alors à nous.

Première approche : injection de la dépendance

On peut bien évidemment injecter un objet de type Clock comme n’importe quelle autre dépendance à nos classes, par exemple en Java en utilisant Guice ou Spring :

// exemple d'utilisation
class Scheduler {
 private final Clock clock;

 @Inject
 Scheduler(Clock clock) {
  this.clock = clock;
 }

 void schedule() {
  if (someDate.before(clock.now())) // alors...
 }
}
 
// exemple de test
class SchedulerTest {

 FixedClock clock = new FixedClock();
 Scheduler scheduler = new Scheduler(clock);

 @Test public void should_schedule_event_when_xxx() {
  // given
  clock.setCurrentDateTime(xxx);

  // when
  scheduler.schedule();

  // then
  // ...
 }
}

Seulement voilà, cela alourdit passablement le code et la configuration pour une donnée que l’on considère habituellement comme acquise. Ne peut-on pas trouver une solution aussi simple à l’usage que : `new Date()` ?

Deuxième approche : introduction d’un singleton configurable

Un "bon vieux" singleton peut également nous permettre de récupérer la bonne implémentation directement dans le code appelant :

public abstract class Clock {

 private static Clock instance = new SystemClock();

 public static Clock get() {
  return instance;
 }

 public static void configureWith(Clock clock) {
  instance = clock;
 }

 protected Clock() {}

 public abstract DateTime currentDateTime();

 // plus pratique à l'usage
 public static DateTime now() {
  return instance.currentDateTime();
 }
}

public class SystemClock extends Clock {
 // ...
}

public class FixedClock extends Clock {
 // ...
}
 
// exemple d'utilisation
class Scheduler {

 void schedule() {
  if (someDate.before(Clock.get().currentDateTime())) // ...
  // ou
  if (someDate.before(Clock.now())) // ...
 }
}
 
// exemple de test
class SchedulerTest {

 Scheduler scheduler = // ...
 FixedClock fixedClock = new FixedClock();

 @Test public void should_schedule_event_when_xxx() {
  // given
  Clock.configureWith(fixedClock);
  fixedClock.setCurrentDateTime(xxx);

  // when
  scheduler.schedule();

  // then
  // ...
 }
}

Le code de Scheduler est à nouveau simple à lire et à écrire, sans sacrifier la testabilité du système. Cependant, on pourra objecter que la méthode permettant de changer l’implémentation retournée par le singleton est un peu trop exposée. Essayons donc d’encapsuler tout cela un peu mieux :

public abstract class Clock {

 // ...

 // package-private
 static void configureWith(Clock clock) {
  instance = clock;
 }

 // ...
}

public class SystemClock extends Clock {
// ...
}

// à placer dans src/test/java/même.package
public class FixedClock extends Clock {

 private static final Clock DEFAULT_CLOCK = Clock.get();
 private static final Clock FIXED_CLOCK = new FixedClock();

 private FixedClock() {}

 public static void install() {
  Clock.configureWith(FIXED_CLOCK);
 }

 public static void uninstall() {
  Clock.configureWith(DEFAULT_CLOCK);
 }
 
 // plus pratique à l'usage
 public static void fixDateTime(DateTime now) {
  install();
  FIXED_CLOCK.setCurrentDateTime(now);
 }

 // ...
}

// exemple de test
class SchedulerTest {

 Scheduler scheduler = // ...

 @Test public void should_schedule_event_when_xxx() {
  // given
  FixedClock.fixDateTime(xxx);

  // when
  scheduler.schedule();

  // then
  // ...
 }

 @After public void restoreDefaultClock() {
  FixedClock.uninstall();
 }
}

Voilà, sauf à utiliser de la réflexion, il sera désormais impossible de changer l’implémentation en dehors du package définisssant Clock. Et en passant, nous avons amélioré la lisibilté de nos tests.

Conclusion

Cette petite technique n’est définitivement pas compliquée à mettre en place, et s’accorde facilement à différentes manières de modulariser son code. Dès lors, il serait dommage de s’en passer et de laisser des trous dans nos jeux de tests !

10 Responses

  • Bonjour,

    notez qu’il existe aussi une façon de fixer le temps présent via Joda Time : DateTimeUtils#setCurrentMillisFixed que l’on peut réinitialiser après chaque test avec DateTimeUtils#setCurrentMillisSystem.

  • Encore plus simple pour les utilisateurs de Joda, en effet !

    Par ailleurs, on me souffle que certains redéfinissent la classe fournissant le temps dans leurs sources de test, mais ça n’est pas accepté par tous les outils de développement (par exemple : Maven le supporte, mais pas Eclipse du fait d’un classpath unique pour les sources principales et de test).

  • Bonjour,

    Pour ma part je suis régulièrement confronté à cette problématique et, s’agissant de systèmes de contrôle d’accréditation, je ne peux pas me permettre d’utiliser l’une des deux technique ci-dessus (NDLR. Lors d’un audit l’utilisation des fonctions natives passe plus facilement).
    J’utilise donc la fonction MockStatic du framework PowerMock qui permet, lors des tests unitaires, de modifier le comportement des méthodes statiques y compris celles du JDK.

    Plus d’info ici : http://code.google.com/p/powermock/wiki/MockSystem

  • Une petite note à ce propos : quand on a le choix, mieux vaut tout de même éviter PowerMock ou limiter son usage sous peine de ralentir l’exécution des tests de manière conséquente !

  • L’approche singleton c’est bien mais des qu’il y a de la concurrence (ou des tests executés en parallèle?) ca me semble un peu compromis.

    Perso même si c’est plus lourd je préfère utiliser la première approche qu’on utilise deja énormément dans Spring et autres frameworks qui ont besoin d’etre extensibles, avec les strategy / factories / providers…
    Pour l’extensibilité et la testabilité mieux vaut éviter d’utiliser new en dehors des factory.

    Y a qu’a rajouter ca:

    @Setter
    CurrentTimeProvider currentTimeProvider = DefaultCurrentTimeProvider.get()

    C’est plus lourd mais bon pas énormément non plus je trouve et ca me semble plus propre et safe que d’utiliser un singleton mutable.

  • J’utilise aussi les mocks (jmockit) : http://virgo47.wordpress.com/tag/jmockit/

    L’avantage est de pouvoir tester des projets existants quelque-soit leur stratégie de récupération de l’heure (l’interface Clock ne faisant pas parti de l’API Java).

  • Le vrai problème ici c’est l’utilisation d’une méthode statique.
    Lorsque l’on appelle Calendar.getInstance() on créé une dépendance forte dans notre code.

    Les méthodes statiques paraissent séduisantes de prime abord, mais à l’utilisation elles deviennent un frein lors de l’écriture des tests (cf. http://www.youtube.com/watch?v=XcT4yYu_TTs).

    Une solution est donc d’extraire la logique dans une classe séparée ayant un attribut Calendar. Il devient ainsi plus aisé de tester unitairement cette classe.

  • Et il s’agit bien de l’idée derrière la première approche présentée ;-)

  • Dans le cadre d’une application web, il peut être intéressant d’avoir cette classe dans un scope session. J’ai déjà mis en place ce type de classe, accompagnée d’une servlet tout simple qui met à jour la date/heure pour l’utilisateur concerné.
    Comme ça, les testeurs de scénarios fonctionnels peuvent modifier la date uniquement pour leur session, sans perturber les autres testeurs.
    C’est simple à mettre en oeuvre, il faut juste être sur que les développeurs ne fassent pas appel à new Date(), Calendar.getInstance(), … (via une règle ajoutée sous checkstyle par exemple).

  • Une autre solution est de déléguer la création des dates à une classe utilitaire « mockable » : http://jsebfranck.blogspot.fr/2013/06/how-to-test-dates-in-java-unit-tests.html

Laisser un commentaire