Pages

vendredi 18 décembre 2015

Tester la date courante en Java

Nous avons vu apparaître avec Java 8 une nouvelle API pour gérer les dates. Cette dernière est fortement inspirée de la librairie JodaTime (JSR 310). D’ailleurs son créateur, Stephen Colebourne a participé à l’implémentation dans le JDK.


En fonction des besoins métiers nous devons parfois implémenter des règles de gestions basées sur l'instant courant. Rien d'anormal jusque là mais comment bien gérer ce cas dans nos tests unitaires ? Le but de cet article est de trouver une réponse à ce problème.


Pourquoi une nouvelle API de date en Java 8

L’API existante avait différents problèmes
  • pas thread safe et donc potentiellement des problèmes de concurrence 
  • API assez pauvre 
  • confusion entre les dates, les heures 
  • … 
Le but de la JSR 310 et de JodaTime (pour les projets avant Java 8) est de fournir les outils pour gérer
  • des objets immutables 
  • les différents systèmes calendaires existants, 
  • les différentes timezones 
  • le formatage et le parsing de dates sans avoir à passer par des classes externes 
  • les différents use cases : parfois nous voulons gérer des périodes, des durées, des dates, des heures… 
Si vous voulez en savoir plus sur Joda Time vous pouvez suivre la documentation en ligne qui est très complète. En gros vous avez 5 classes principales (on les retrouve à la fois dans Joda Time et dans le JDK >=8)

Instant (jodajdk
Représente un point dans la ligne de temps parfait pour gérer les tmestamps “techniques” qui ne sont pas liés à une time zone

DateTime (joda), ZonedDateTime (jdk)
Remplace l’objet Calendar et représente une date dans une timezone

LocalDate (jodajdk)
Représente une date sans heure et sans time zone

LocalTime (joda, jdk
Représente une heure sans notion de date et de time zone

LocalDateTime (joda, jdk) 
Représente une date/heure sans time zone
Il existe plusieurs autres classes utilitaires pour vous aider à gérer des durées (Duration - jodajdk), des périodes (Period - joda, jdk), ...

Exemple en JDK8

private Instant eventDate = Instant.parse("2016-04-21T08:30:00.00Z");

public String getCountDownBeforeEvent(){
  if(Instant.now().compareTo(eventDate)<0){
    return String.format("%s days before our event"
      Duration.between(Instant.now(), eventDate).toDays());
  }
  return String.format("Event closed since %s days"
    Duration.between(eventDate, Instant.now()).toDays());
}

et en Joda Time le code est légèrement différent

public String getCountDownBeforeEvent() {
  if (Instant.now().compareTo(eventDate) < 0) {
    return String.format("%s days before our event",
      new Duration(Instant.now().getMillis(), eventDate.getMillis())
        .toStandardDays().getDays());
  }
  return String.format("Event closed since %s days",
    new Duration(eventDate.getMillis(), Instant.now().getMillis())
      .toStandardDays().getDays());
}

Fixer la date dans les tests Joda Time

JodaTime propose DateTimeUtils, une classe utilitaire très pratique lorsque vous voulez tester une portion de code manipulant une date courante. Cette classe est en fait plus qu’une classe utilisée pour les tests. Tous les objets Joda se basent sur cette classe pour déterminer l'instant courant. 

DateTimeUtils expose des méthodes qui permettent de mocker la valeur de l’instant courant dans les tests. Cette implémentation n’a pas été reprise dans la JSR.

Le code exposé dans le précédent paragraphe se teste très simplement en fixant la valeur du currentTimeMillis le temps du test

public class ServiceJodaTimeTest {
    
  private ServiceJodaTime service = new ServiceJodaTime();

  @After
  public void tearDown(){
    DateTimeUtils.setCurrentMillisSystem();
  }

  @Test
  public void should_test_a_date_before_event() {
    DateTimeUtils.setCurrentMillisFixed(Instant.parse("2016-04-18T00:00:00.00Z").getMillis());
    Assertions.assertThat(service.getCountDownBeforeEvent()).isEqualTo("3 days before our event");
  }

  @Test
  public void should_test_a_date_after_event() {
    DateTimeUtils.setCurrentMillisFixed(Instant.parse("2016-04-30T00:00:00.00Z").getMillis());
    Assertions.assertThat(service.getCountDownBeforeEvent()).isEqualTo("Event closed since 8 days");
  }
}

Comment faire en Java 8

Côté Java, comme je le disais avant la classe DateTimeUtils n’a pas été reprise. En fait si on regarde dans le code l’instant présent en Java se détermine en appelant l’objet java.time.Clock. Par exemple

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

Vous pouvez aussi si vous le souhaitez utiliser votre propre instance de Clock. Dans la Javadoc c’est cette méthode qui est préconisée en cas de tests

/**
 * Obtains the current instant from the specified clock.
 * <p>
 * This will query the specified clock to obtain the current time.
 * <p>
 * Using this method allows the use of an alternate clock for testing.
 * The alternate clock may be introduced using {@link Clock dependency injection}.
 *
 * @param clock  the clock to use, not null
 * @return the current instant, not null
 */
public static Instant now(Clock clock) {
  Objects.requireNonNull(clock, "clock");
  return clock.instant();
}

Si on suit donc les préconisations il faudrait reprendre notre code principal

private Clock clock = Clock.systemUTC();

public String getCountDownBeforeEvent2(){

  if(Instant.now(clock).compareTo(eventDate)<0){
    return String.format("%s days before our event"
      Duration.between(Instant.now(clock), eventDate).toDays());
  }
  return String.format("Event closed since %s days"
      Duration.between(eventDate, Instant.now(clock)).toDays());
}

Dans notre test nous pouvons maintenant fixer la date

public class ServiceJava8Test {

    private ServiceJava8 service = new ServiceJava8();

    @After
    public void tearDown(){
        service.setClock(Clock.systemUTC());
    }

    @Test
    public void should_test_a_date_before_event() {
        service.setClock(Clock.fixed(Instant.parse("2016-04-18T00:00:00.00Z"), ZoneId.systemDefault()));
        Assertions.assertThat(service.getCountDownBeforeEvent2()).isEqualTo("3 days before our event");
    }

    @Test
    public void should_test_a_date_after_event() {
        service.setClock(Clock.fixed(Instant.parse("2016-04-30T00:00:00.00Z"), ZoneId.systemDefault()));
        Assertions.assertThat(service.getCountDownBeforeEvent2()).isEqualTo("Event closed since 8 days");
    }

}

Voila vous savez maintenant tout pour tester correctement la date courante dans vos tests. Mes exemples pourraient être améliorer en introduisant par exemple une Rule Junit pour réinitialiser automatiquement la date à la fin du test. Si vous utilisez une version du JDK < 8 je vous conseille vivement d’utiliser JodaTime. 

Tout le code présenté ici est disponible sous Github

Aucun commentaire:

Enregistrer un commentaire

Remarque : Seul un membre de ce blog est autorisé à enregistrer un commentaire.