Zeitreise mit LocalDate und DateTimeFormatter

Die Krux mit den Patterns

Meist sind plötzlich fehlschlagene Tests in einem Projekt auf vorangegangene Codeänderungen zurückzuführen. Manchmal ist der Zusammenhang offensichtlich erkennbar, manchmal liegen zwischen dem geänderten Code und der problematischen Stelle im Test mehrere Aufrufe oder Indirektionen. Für gewöhnlich lässt sich der Grund für einen fehlschlagenen Test aber im letzten Commit bzw. dem Diff zum letzten eingecheckten Stand finden. Kurios sind solche Tests, die völlig ohne Zusammenhang von einem auf den anderen Tag fehlschlagen. Auf einen solchen Test sind wir gestern im einem unserer Projekte gestoßen.

Aufbau des Test-Szenarios

Getestet wird in diesem Fall ein Validator. Dieser prüft eine Datumsangabe auf ein bestimmtes Format und darauf, dass das Datum nicht in der Zukunft liegt. Um die relevanten Testwerte abzudecken, wird ein LocalDate von „heute“ erzeugt und für die unterschiedlichen Assertions mit minusMonths(1) und plusMonths(1) entsprechend manipuliert. Anschließend wird dieses Datum mit Hilfe eines DateTimeFormatter in den passenden String formatiert.

Der zugehörige Test-Code sieht etwa folgendermaßen aus:

private String monthYear(LocalDate date) {
    return date.format(DateTimeFormatter.ofPattern("MM/YYYY"));
}

@Test
public void testDates() {
    LocalDate today = LocalDate.now();
    LocalDate past = today.minusMonths(1);
    LocalDate future = today.plusMonths(1);
    DateValidator validator = new DateValidator();
    assertTrue("date not in past", validator.validate(monthYear(past)));
    assertFalse("date is in future", validator.validate(monthYear(future)));
}

Ich kann vorweg nehmen, dass der Grund für das Fehlschlagen dieses Tests nicht in der Implementierung des Validators liegt. Das Problem liegt im Test-Code. Wer die Ursache noch nicht erkannt hat, darf sich im Folgenden auf die Erklärung freuen. Wichtig ist jedoch noch zu beachtet, dass der Test lediglich gestern am 31.01.2019 fehl schlug, nicht aber am 30. Januar oder davor.

Analyse und Debugging

Der Code sieht auf den ersten Blick unverdächtig aus. Vom heutigen Datum wird ein Monat abgezogen, das Ganze wird in einen String formatiert und validiert. Wieso also ist das Datum nicht in der Vergangenheit? Mein Verdacht lag schnell auf der Formatierung des Datums, aber wir haben den Debugger bemüht, um dies zu bestätigen. Die Variable past enthielt wie erwartet den 31.12.2018, aber dem Validator wurde der String „12/2019“ übergeben. Seltsam, aber die Dokumentation sollte uns eine Erklärung liefern. Wir schauten also in die Docs des DateTimeFormatter. Für die Formatierung des Jahres gibt es dort drei mögliche Angaben:

  • uuuu gibt das Jahr als vierstellige Zahl an
  • yyyy gibt das Jahr seit Beginn einer Ära an
  • YYYY gibt das Jahr basierend auf den Kalenderwochen an

Und genau hier lag das Problem. Die KW1 für 2019 begann bereits am 31.12.2018. Damit formatierte der Formatter aus dem Datum den String „12/2019“. Im Nachhinein eine ganz klare Sache. Die Falle an der Stelle lag also daran, dass die Wochenzählung für 2019 bereits im vergangenen Jahr begann. Hinzu kommt die Inkonsistenz zwischen verschiedenen Programmiersprachen. Schaut man sich beispielsweise JavaScript an, entsprechen dort vier große YYYY dem Jahr mit Jahrtausend. In Ruby erhalte ich mit %Y das volle Jahr und in C# verwende ich dazu yyyy. Da kann es auch schon mal zu Verwechslungen kommen. Den Kollegen, die den Test ursprünglich implementiert haben, ist also auch keinesfalls ein Vorwurf zu machen. Es lohnt sich aber, bei der Formatierung des Datums immer nochmal die Dokumentation zu prüfen, ob man denn auch die richtigen Zeichen gewählt hat, um das gewünschte Format zu erhalten.

Übrigens: nächstes Jahr beginnt die KW1 bereits am 30.12.2019. Danach verschiebt sich der Beginn von KW1 durch das Schaltjahr in 2020 erstmal wieder in den Januar.

TAGS

Comments

Please accept our cookie agreement to see full comments functionality. Read more