Override Java System.currentTimeMillis for testing time sensitive code

Viewed 118276

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?

12 Answers

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.

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...

Use Aspect-Oriented Programming (AOP, for example AspectJ) to weave the System class to return a predefined value which you could set within your test cases.

Or weave the application classes to redirect the call to System.currentTimeMillis() or to new Date() to another utility class of your own.

Weaving system classes (java.lang.*) is however a little bit more trickier and you might need to perform offline weaving for rt.jar and use a separate JDK/rt.jar for your tests.

It's called Binary weaving and there are also special tools to perform weaving of System classes and circumvent some problems with that (e.g. bootstrapping the VM may not work)

There really isn't a way to do this directly in the VM, but you could all something to programmatically set the system time on the test machine. Most (all?) OS have command line commands to do this.

If you're running Linux, you can use the master branch of libfaketime, or at the time of testing commit 4ce2835.

Simply set the environment variable with the time you'd like to mock your java application with, and run it using ld-preloading:

# bash
export FAKETIME="1985-10-26 01:21:00"
export DONT_FAKE_MONOTONIC=1
LD_PRELOAD=/usr/local/lib/faketime/libfaketimeMT.so.1 java -jar myapp.jar

The second environment variable is paramount for java applications, which otherwise would freeze. It requires the master branch of libfaketime at the time of writing.

If you'd like to change the time of a systemd managed service, just add the following to your unit file overrides, e.g. for elasticsearch this would be /etc/systemd/system/elasticsearch.service.d/override.conf:

[Service]
Environment="FAKETIME=2017-10-31 23:00:00"
Environment="DONT_FAKE_MONOTONIC=1"
Environment="LD_PRELOAD=/usr/local/lib/faketime/libfaketimeMT.so.1"

Don't forget to reload systemd using `systemctl daemon-reload

Here is an example using PowerMockito. Also there is an example with new Date().
More details about mocking system classes.

@RunWith(PowerMockRunner.class)
@PrepareForTest(LegacyClass.class)
public class SystemTimeTest {
    
    private final Date fakeNow = Date.from(Instant.parse("2010-12-03T10:15:30.00Z"));

    @Before
    public void init() {
        PowerMockito.mockStatic(System.class);
        PowerMockito.when(System.currentTimeMillis()).thenReturn(fakeNow.getTime());
        System.out.println("Fake currentTimeMillis: " + System.currentTimeMillis());
    }

    @Test
    public void legacyClass() {
        new LegacyClass().methodWithCurrentTimeMillis();
    }

}

Some legacy class you are testing:

class LegacyClass {

    public void methodWithCurrentTimeMillis() {
        long now = System.currentTimeMillis();
        System.out.println("LegacyClass System.currentTimeMillis() is " + now);
    }

}

Console output

Fake currentTimeMillis: 1291371330000
LegacyClass System.currentTimeMillis() is 1291371330000
Related