Is it safe to use DateTimeUtils.setCurrentMillisFi

2020-08-09 07:15发布

问题:

In order to test time-dependent code it's good to use Virtual Clock Pattern

The idea is that we pull current time not using new Date, but from a clock which can be mocked with virtual clock returning predefined fixed time.

Now in Java we have JodaTime with DateTime class and which allows to set sample time with

DateTimeUtils.setCurrentMillisFixed(today.getMillis());

And reset the fixed time to a system time with:

DateTimeUtils.setCurrentMillisSystem();

Here is a good article on how to use it with TestNG.

Now the question!

How safe it's to use this technique with setUp and tearDown methods if it globally sets fixed time in global context for the time of running test. As long as I get it - it will only work as long as we don't have two concurrent tests with this technique running in the same environment in parallel.

回答1:

You must ensure that DateTimeUtils.setCurrentMillisSystem() is invoked in the tearDown method. So that one test does not affect another. TestNG should invoke tearDown even if an exception occurs in your test.

I often prefer another way when I want to decouple a class from System.currentTimeMillis();. I introduce an interface Clock and one implementation SystemClock like this:

public interface Clock {
    public long getCurrentTimeMillis();
}

public class SystemClock implements Clock {
    public long getCurrentTimeMillis(){
        return System.currentTimeMillis();
    }
}

For the tests it is then easy to create a mock that either returns a fixed time on every invocation or a series of predefined times.

Some might argue that it is overengineering to introduce such an interface to decouple only one method and it would be a performance impact. But fortunately we have a JIT compiler and since JIT knows that only the SystemClock class is loaded it knows that no other implementation exist (at the moment). At this assumption it can just use inline method.

So I prefer to write code in the way it can be tested best.

EDIT

With Java 8 you might want to use the Supplier<Long> interface.

E.g. in your client code you can than use method references

public class SomeClass {
    private Supplier<Long> currentTimeMillisSupplier;

    public SomeClass(){
         this(System::currentTimeMillis);
    }

    SomeClass(Supplier<Long> currentTimeMillisSupplier){
        this.currentTimeMillisSupplier = currentTimeMillisSupplier;
    }
}

The default constructor is for 'normal' use, while the other package scoped constructor can be used for unit tests. Just place the test class in the same package.

You can also use the Clock interface, because it is a @FunctionalInterface.

public class SomeClass {
    private Clock clock;

    public SomeClass(){
         this(System::currentTimeMillis);
    }

    public SomeClass(Clock clock){
        this.clock = clock;
    }
}