How often in Java services do we need to use the current date and time? Most of us would agree we use it quite often. Many times, we may want to record when an event happened, such as logging in, sending an invoice, or recording a chat. Other times, we may want to use the current time in a business rule to determine if a date/time being entered is in the future or the past, or maybe we want to check to see if something has expired.
Many versions of Java ago, there was java.util.Date
; we could simply capture the current time using new Date()
. Then, there was java.util.Calendar
that dealt with many of the calendar issues that java.util.Date
did not, and you could simply use Calendar.getInstance()
.
Now, we have java.time
packages, and zones are fully handled for us by Java. But even with this support, testing services that depend on the current date and time still present an issue.
This blog will provide a simple approach to testing services that depend on the current date and/or time. It is specifically written for Java Spring developers and does not require Aspect Oriented Programming. Instead, we will use a few simple classes incorporated into our services and tests.
Java.Time
There are many classes in the java.time
package for dealing with dates and times, but this discussion will focus on java.time.ZonedDateTime
. It consists of three components:
Date:
Year, Month, DayTime:
Hours, Minutes, Seconds, and (optionally) MillisecondsZoneId:
The Time Zone of the date and time
Many applications persist all the ZonedDateTime
values in a consistent ZoneId
, like UTC (Universal Time Coordinated). So, when displaying a time to the user, the time is converted to that user’s particular time zone. When creating an instance of ZonedDateTime
, we can explicitly set the time zone, or we can use ZoneId.systemDefault()
. This default can be set up in your application config using…
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
A very common way to get the current time with the default time zone is shown below.
ZonedDateTime currentTime = ZonedDateTime.now();
This approach looks good, but there is a problem when it comes time to test. Let’s start with an example.
An Example of Time-Based Business Rule
Let’s assume you are writing an application that gives a discount for tacos purchased on Tuesdays. Your method for computing an order’s price may look something like this:
This method should give us the correct discount every time it is run on a Tuesday. But wait! If we are running a test for this class, I want a scenario for when it’s Tuesday and another for when it’s not, and I certainly do not want a different test class that is only run on certain days of the week.
This is our problem to solve.
Possible Solutions
Let’s brainstorm possible solutions to our problem.
AOP: Aspect-Oriented Programming
This approach is very well documented (see this resource) and is the least intrusive on existing code; it may well be the approach you prefer. However, if your developers are not familiar with the intricacies of the AOP framework, a simpler but more intrusive approach may be better.
Insisting on Using ZonedDateTime.now(Clock)
This approach gives the advantage of using a Clock
object from which the current time is acquired. Using Spring, we can inject the class of Clock
to use at test time and achieve similar results as the next option. However, we are then limited to the exact approach for getting current time; a more flexible approach follows.
Introduction of a CurrentDateTimeService
As you may have guessed, this is the approach being recommended. Let’s get the negatives out of the way first.
This approach is intrusive; it requires you to change all of the places you currently get the time to use this service. How difficult this service is to implement depends on how many changes are required, but you only need to change existing code to take advantage of easier testing, or, if you’re like me, to keep the code consistent throughout your application.
As for the positives, they are the highlights of the next section.
The CurrentDateTimeService
Interface
In Spring, Java developers are accustomed to auto-wiring services that have implementations configured differently for runtime vs. test-time. This approach requires that each time the current time is required in a service, instead of calling ZonedDateTime.now()
, you call this new service:
Let’s focus on the first two methods. The first one, now()
, is what you might expect. You may have already figured out that you would replace ZonedDateTime.now()
with currentDateTimeService.now()
, and perhaps you have already included the following as well.
@Autowired private CurrentDateTimeService currentDateTimeService;
Not difficult conceptually, but it is a change to existing code.
The second method, nowForZone(…)
, is simply a convenience method for what you saw in the Taco Tuesday example above.
ZonedDateTime.now().withZoneSameInstant(restaurantLocation.getZoneId());
Instead:
currentDateTimeService.nowForZone(restaurantLocation.getZoneId());
The remaining methods (seen below) are also convenience methods that you may or may not need.
The first two allow you to leave legacy implementations that may still be using the old ways of dealing with date/time values. Your service will still be using Date or Calendar values, but the producer of the current times will get all of the test advantages of this new approach.
The latter two methods are similar to the now()
and nowforZone()
methods except that they deal only with date or time, but not both. These situations also require a technique such as this for testing.
To summarize, this new service is a required change to your application for how current date/time values are produced. The runtime portion of your application should work exactly the same after making these changes.
So, why do it? Let me explain.
Implementation of CurrentDateTimeService
Interface
Where is the magic happening? The answer is in the implementation. The main service implementation looks like this:
No real magic here, beyond the normal Spring injection. This should be the exact implementation you are doing in your existing code for these cases.
The magic is in the alternate mock implementation used in tests. Let’s first explore that implementation.
Mock implementation
This implementation class implements our interface CurrentDateTimeService
and includes a Spring @Service annotation with a qualified name that will help us choose a different implementation during the test setup. Note that this implementation should be in src/test/java, not src/main/java, or in a dependency with test scope to avoid conflicts at runtime.
The magic starts with the declaration of a localClock
that uses a ThreadLocal
to hold a java.time.Clock
. The threadlocal
is used to prevent problems with concurrent test runs.
The main focus for us is the Clock class. This class, as you will soon see, allows us to fix the time at a specific moment for testing.
Fixed time using java.time.Clock
These methods are used to control the clock used in providing the current time. Having control over the current time is what we are trying to achieve, and these methods give us that control. By default, the normal system clock is used. The control happens when you call setCurrentFixedTime(…)
with an ISO_DATE_TIME formatted string or with an existing ZonedDateTime
. An example of the string format is 2011-12-03T10:15:30+01:00
.
We will look at how these are used shortly, but let’s first look at the remaining methods.
Mocked Methods
The remaining methods are all annotated with @Override
, which means they are the ones from our interface, and here is how they are implemented. Since they all use the local clock and we have control over the local clock, we can control what time NOW
is.
The next section shows an example of using this approach.
CurrentDateTimeService
Example
The following is a Cucumber feature file showing our test for Taco Tuesday.
The scenarios above allow us to test on whatever dates and times we would like. In this case, we test on 2023-08-08, a Tuesday, and on 2023-08-10, a Thursday, and get different results, as expected.
This date-time step is set up in the cucumber StepDefinitions.java
file as follows.
Wrap Up
Wrapping up, we covered using and testing the current date/time in Java and Spring. We started by creating the Interface and normal Impl somewhere in our src/main/java folder, and then we auto-wired the interface into our service and used the interface methods for now()
. Finally, we wrote tests that used the mock Impl and set the current date times in our tests.
That’s all there is to it! Nice work.
I hope this is helpful as you build and test Java Spring applications. If you have questions or want a copy of the code used in this blog, please let me know. You can find me at [email protected], and I will also respond to the comments below.
As always, if you enjoyed this post and want more, check out our content on the Keyhole Dev Blog. Thanks!