Here is a simple test example to show what fails and when. In short, I mock a static now() method of the Instance class but later on there is another call of this mocked instance, - minus() method which will return null:
@Test
void givenInstantMock_whenNow_thenGetFixedInstant() {
String instantExpected = "2014-12-22T10:15:30Z";
Clock clock = Clock.fixed(Instant.parse(instantExpected), ZoneId.of("UTC"));
Instant instant = Instant.now(clock);
try (MockedStatic<Instant> mockedStatic = mockStatic(Instant.class)) {
mockedStatic.when(Instant::now).thenReturn(instant);
Instant now = Instant.now();
assertThat(now.toString()).isEqualTo(instantExpected);
Instant earlier2weeks = now.minus(Period.ofWeeks(2));
assertNotNull(earlier2weeks);// FAILS as it will be null
}
}
In the real situation, a service class creates a new Instant with Instant::now:
//in a component class
@Component
class DeletionScheduler {
...
public void findOutdatedEntries() {
Instant limitDate = Instant.now().minus(validityGracePeriod);
List<Entry> entriedToDelete= shareService.findOutdatedEtriees(limitDate );
...
}
So as you can see, even if I mock the Instant.now() the call to minus on this instance will always return NULL.
So the idea of the test was:
- to create an entry with a `limitDate` in the future.
- trick/mock the System clock to be in the future
- check if there are some outdated entries.
Is there any way to get around it?
This is because all static method of Instant is returning null within the try block, the behaviour is the same as we do for instance method in instance mock.
If we debug into the minus call, it will finally arrive ofEpochSecond which will return null.
private Instant plus(long secondsToAdd, long nanosToAdd) {
if ((secondsToAdd | nanosToAdd) == 0) {
return this;
}
long epochSec = Math.addExact(seconds, secondsToAdd);
epochSec = Math.addExact(epochSec, nanosToAdd / NANOS_PER_SECOND);
nanosToAdd = nanosToAdd % NANOS_PER_SECOND;
long nanoAdjustment = nanos + nanosToAdd; // safe int+NANOS_PER_SECOND
return ofEpochSecond(epochSec, nanoAdjustment);
}
You can provide setting for defaultAnswer to call real method by default.
try (MockedStatic<Instant> mockedStatic = mockStatic(Instant.class,
withSettings().defaultAnswer(invocation -> invocation.callRealMethod()))) {
mockedStatic.when(Instant::now).thenReturn(instant);
...
}
referring to Clock javadoc
The primary purpose of this abstraction is to allow alternate clocks to be plugged in as and when required. Applications use an object to obtain the current time rather than a static method. This can simplify testing.
And suggested by bowmore, we should treat Clock as a service, and set the time as desired.
In addition, static method is global by default, hence changing(static mock) it behaviour is as error prone as changing a global variable.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With