Override Java System.currentTimeMillis for testing time sensitive code
Is there a way, either in code or with JVM arguments, to override the current time, as presented via System.currentTimeMillis
, other than manually changing the system clock on the host machine?
A little background:
We have a system that runs a number of accounting jobs that revolve much of their logic around the current date (ie 1st of the month, 1st of the year, etc)
Unfortunately, a lot of the legacy code calls functions such as new Date()
or Calendar.getInstance()
, both of which eventually call down to System.currentTimeMillis
.
For testing purposes, right now, we are stuck with manually updating the system clock to manipulate what time and date the code thinks that the test is being run.
So my question is:
Is there a way to override what is returned by System.currentTimeMillis
? For example, to tell the JVM to automatically add or subtract some offset before returning from that method?
Thanks in advance!
Solution 1:
I strongly recommend that instead of messing with the system clock, you bite the bullet and refactor that legacy code to use a replaceable clock. Ideally that should be done with dependency injection, but even if you used a replaceable singleton you would gain testability.
This could almost be automated with search and replace for the singleton version:
- Replace
Calendar.getInstance()
withClock.getInstance().getCalendarInstance()
. - Replace
new Date()
withClock.getInstance().newDate()
- Replace
System.currentTimeMillis()
withClock.getInstance().currentTimeMillis()
(etc as required)
Once you've taken that first step, you can replace the singleton with DI a bit at a time.
Solution 2:
tl;dr
Is there a way, either in code or with JVM arguments, to override the current time, as presented via System.currentTimeMillis, other than manually changing the system clock on the host machine?
Yes.
Instant.now(
Clock.fixed(
Instant.parse( "2016-01-23T12:34:56Z"), ZoneOffset.UTC
)
)
Clock
In java.time
We have a new solution to the problem of a pluggable clock replacement to facilitate testing with faux date-time values. The java.time package in Java 8 includes an abstract class java.time.Clock
, with an explicit purpose:
to allow alternate clocks to be plugged in as and when required
You could plug in your own implementation of Clock
, though you likely can find one already made to meet your needs. For your convenience, java.time includes static methods to yield special implementations. These alternate implementations can be valuable during testing.
Altered cadence
The various tick…
methods produce clocks that increment the current moment with a different cadence.
The default Clock
reports a time updated as frequently as milliseconds in Java 8 and in Java 9 as fine as nanoseconds (depending on your hardware). You can ask for the true current moment to be reported with a different granularity.
-
tickSeconds
- Increments in whole seconds -
tickMinutes
- Increments in whole minutes -
tick
- Increments by the passedDuration
argument.
False clocks
Some clocks can lie, producing a result different than that of the host OS’ hardware clock.
-
fixed
- Reports a single unchanging (non-incrementing) moment as the current moment. -
offset
- Reports the current moment but shifted by the passedDuration
argument.
For example, lock in the first moment of the earliest Christmas this year. in other words, when Santa and his reindeer make their first stop. The earliest time zone nowadays seems to be Pacific/Kiritimati
at +14:00
.
LocalDate ld = LocalDate.now( ZoneId.of( "America/Montreal" ) );
LocalDate xmasThisYear = MonthDay.of( Month.DECEMBER , 25 ).atYear( ld.getYear() );
ZoneId earliestXmasZone = ZoneId.of( "Pacific/Kiritimati" ) ;
ZonedDateTime zdtEarliestXmasThisYear = xmasThisYear.atStartOfDay( earliestXmasZone );
Instant instantEarliestXmasThisYear = zdtEarliestXmasThisYear.toInstant();
Clock clockEarliestXmasThisYear = Clock.fixed( instantEarliestXmasThisYear , earliestXmasZone );
Use that special fixed clock to always return the same moment. We get the first moment of Christmas day in Kiritimati, with UTC showing a wall-clock time of fourteen hours earlier, 10 AM on the prior date of the 24th of December.
Instant instant = Instant.now( clockEarliestXmasThisYear );
ZonedDateTime zdt = ZonedDateTime.now( clockEarliestXmasThisYear );
instant.toString(): 2016-12-24T10:00:00Z
zdt.toString(): 2016-12-25T00:00+14:00[Pacific/Kiritimati]
See live code in IdeOne.com.
True time, different time zone
You can control which time zone is assigned by the Clock
implementation. This might be useful in some testing. But I do not recommend this in production code, where you should always specify explicitly the optional ZoneId
or ZoneOffset
arguments.
You can specify that UTC be the default zone.
ZonedDateTime zdtClockSystemUTC = ZonedDateTime.now ( Clock.systemUTC () );
You can specify any particular time zone. Specify a proper time zone name in the format of continent/region
, such as America/Montreal
, Africa/Casablanca
, or Pacific/Auckland
. Never use the 3-4 letter abbreviation such as EST
or IST
as they are not true time zones, not standardized, and not even unique(!).
ZonedDateTime zdtClockSystem = ZonedDateTime.now ( Clock.system ( ZoneId.of ( "America/Montreal" ) ) );
You can specify the JVM’s current default time zone should be the default for a particular Clock
object.
ZonedDateTime zdtClockSystemDefaultZone = ZonedDateTime.now ( Clock.systemDefaultZone () );
Run this code to compare. Note that they all report the same moment, the same point on the timeline. They differ only in wall-clock time; in other words, three ways to say the same thing, three ways to display the same moment.
System.out.println ( "zdtClockSystemUTC.toString(): " + zdtClockSystemUTC );
System.out.println ( "zdtClockSystem.toString(): " + zdtClockSystem );
System.out.println ( "zdtClockSystemDefaultZone.toString(): " + zdtClockSystemDefaultZone );
America/Los_Angeles
was the JVM current default zone on the computer that ran this code.
zdtClockSystemUTC.toString(): 2016-12-31T20:52:39.688Z
zdtClockSystem.toString(): 2016-12-31T15:52:39.750-05:00[America/Montreal]
zdtClockSystemDefaultZone.toString(): 2016-12-31T12:52:39.762-08:00[America/Los_Angeles]
The Instant
class is always in UTC by definition. So these three zone-related Clock
usages have exactly the same effect.
Instant instantClockSystemUTC = Instant.now ( Clock.systemUTC () );
Instant instantClockSystem = Instant.now ( Clock.system ( ZoneId.of ( "America/Montreal" ) ) );
Instant instantClockSystemDefaultZone = Instant.now ( Clock.systemDefaultZone () );
instantClockSystemUTC.toString(): 2016-12-31T20:52:39.763Z
instantClockSystem.toString(): 2016-12-31T20:52:39.763Z
instantClockSystemDefaultZone.toString(): 2016-12-31T20:52:39.763Z
Default clock
The implementation used by default for Instant.now
is the one returned by Clock.systemUTC()
. This is the implementation used when you do not specify a Clock
. See for yourself in pre-release Java 9 source code for Instant.now
.
public static Instant now() {
return Clock.systemUTC().instant();
}
The default Clock
for OffsetDateTime.now
and ZonedDateTime.now
is Clock.systemDefaultZone()
. See source code.
public static ZonedDateTime now() {
return now(Clock.systemDefaultZone());
}
The behavior of the default implementations changed between Java 8 and Java 9. In Java 8, the current moment is captured with a resolution only in milliseconds despite the classes’ ability to store a resolution of nanoseconds. Java 9 brings a new implementation able to capture the current moment with a resolution of nanoseconds – depending, of course, on the capability of your computer hardware clock.
About java.time
The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date
, Calendar
, & SimpleDateFormat
.
To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.
The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.
You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.*
classes. Hibernate 5 & JPA 2.2 support java.time.
Where to obtain the java.time classes?
-
Java SE 8, Java SE 9, Java SE 10, Java SE 11, and later - Part of the standard Java API with a bundled implementation.
- Java 9 brought some minor features and fixes.
-
Java SE 6 and Java SE 7
- Most of the java.time functionality is back-ported to Java 6 & 7 in ThreeTen-Backport.
-
Android
- Later versions of Android (26+) bundle implementations of the java.time classes.
- For earlier Android (<26), a process known as API desugaring brings a subset of the java.time functionality not originally built into Android.
- If the desugaring does not offer what you need, the ThreeTenABP project adapts ThreeTen-Backport (mentioned above) to Android. See How to use ThreeTenABP….
Solution 3:
As said by Jon Skeet:
"use Joda Time" is almost always the best answer to any question involving "how do I achieve X with java.util.Date/Calendar?"
So here goes (presuming you've just replaced all your new Date()
with new DateTime().toDate()
)
//Change to specific time
DateTimeUtils.setCurrentMillisFixed(millis);
//or set the clock to be a difference from system time
DateTimeUtils.setCurrentMillisOffset(millis);
//Reset to system time
DateTimeUtils.setCurrentMillisSystem();
If you want import a library that has an interface (see Jon's comment below), you could just use Prevayler's Clock, which will provide implementations as well as the standard interface. The full jar is only 96kB, so it shouldn't break the bank...