“Well, it’s Groundhog Day… again…”
Phil Connors
The moment is a wonderful thing, but not for software developers, because it keeps on moving. How can you write a test if the value changes with every test run? Functionality has always been an abomination that concealed some form of now. In FreshMarker, for example, this little monster can be found in the built-in variable .now
.
This is now: ${.now}
This small fragment of a template has not yet been tested well. An approximation of a good test is to check whether the output time can be found between the start and end of the test. What we need here is the ability to manipulate the time.
case "now" -> new TemplateZonedDateTime(ZonedDateTime.now(context.getZoneId()))
Previously, the value for the .now
variable was generated using the ZonedDateTime#now
method. To keep this post short, we can mock the static call with Mokito and we have solved our initial problem. Fortunately for the reader, the initial problem is always just the starting point for a somewhat more in-depth consideration of a wider use-case.
A much more interesting way to manipulate time is to realize a time travel. Not only do we want to ensure in a test that our variable contains the value of ZonedDateTime#now
, but the processing in the template engine should work with past and future time values for .now
. Typical use cases for such a feature are, of course, tests, but also the regeneration of historical content or the early generation of content required in the future.
Fortunately, the developers of the Java Time API have also thought about this problem and included the Clock
into the JDK. The Clock
can be used to query the time in form of an Instant
or milliseconds. The current Clock
can be accessed via Clock#systemUTC
and Clock#systemDefaultZone
. For the temporal classes such as ZonedDateTime
, there are now
methods to which a Clock
instance can be passed.
Now the question is probably how this will help us at this point. So far, it just looks like another useless indirection. The interesting aspect is that other Clock
implementations can be used and the JDK already provides helpful implementations. Some use cases can be solved with the FixedClock
. The FixedClock
is not a real clock because it always returns the same point in time. Not really a clock, but exactly what we need. In the test, this Clock
provides exactly the value we give it because it does not advance in time.
Instant instant = ZonedDateTime.of(1968, 8, 24, 12, 30, 0, 0, ZoneOffset.UTC).toInstant(); Clock clock = Clock.fixed(instant, ZoneOffset.UTC); ZonedDateTime dateTime = ZonedDateTime.now(clock);
In this case, we use August 24, 1968 at 12:30 p.m. and thus create a FixedClock
. Later, we can use it to create our current time as a ZonedDateTime
instance.
The conversion for the built-in variable is not much more complex than the previous implementation. Instead of the zone ID, the Clock
is returned from the context. The Clock
or Clock#systemDefaultZone
stored in the TemplateBuilder
is used here. In both cases, the Clock
is modified with the current zone ID.
case "now" -> new TemplateZonedDateTime(ZonedDateTime.now(context.getClock()));
So that a Clock
can be stored in the TemplateBuilder, we still need a method withClock
. A test with this method looks like the following example.
Instant instant = ZonedDateTime.of(1968, 8, 24, 12, 30, 0, 0, ZoneOffset.UTC).toInstant(); templateBuilder = new Configuration().builder().withClock(Clock.fixed(instant, ZoneOffset.UTC)); Template template = templateBuilder.getTemplate("test", "${.now}"); assertEquals("1968-08-24 01:30:00 Europe/Berlin", template.process(Map.of())); TimeUnit.SECONDS.sleep(5); assertEquals("1968-08-24 01:30:00 Europe/Berlin", template.process(Map.of()));
A FixedClock
was stored in the TemplateBuilder
, so the first call returns 1968-08-24 01:30:00 Europe/Berlin
. The second call 5 seconds later then returns the identical time.
If you want to use a Clock
that moves in the past or future and does not stand still, you can also use the OffsetClock
for your use case. It is created with the Clock#offset
method.
long currentEpochDay = Localdate.now() long fluxEpochDay = LocalDate.of(1968, Month.AUGUST, 24); Clock fluxClock = Clock.offset(clock, Duration.ofDays(fluxEpochDay - currentEpochDay));
In this example, a clock is generated that begins to strike on August 24, 1968. If you want to use your own clock with FreshMarker, you can try it out with version 1.6.9 on Maven Central.