Override Java System.currentTimeMillis for testing time sensitive code Override Java System.currentTimeMillis for testing time sensitive code java java

Override Java System.currentTimeMillis for testing time sensitive code


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() with Clock.getInstance().getCalendarInstance().
  • Replace new Date() with Clock.getInstance().newDate()
  • Replace System.currentTimeMillis() with Clock.getInstance().currentTimeMillis()

(etc as required)

Once you've taken that first step, you can replace the singleton with DI a bit at a time.


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.

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 passed Duration 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?


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 timeDateTimeUtils.setCurrentMillisFixed(millis);//or set the clock to be a difference from system timeDateTimeUtils.setCurrentMillisOffset(millis);//Reset to system timeDateTimeUtils.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...