Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mockito: using mockStatic() - how to mock instance methods on the scoped mock object

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?
 
like image 953
belgoros Avatar asked Jun 05 '26 09:06

belgoros


1 Answers

Explanation

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);
    }

Workaround

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);
     ...
}

Better solution

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.

like image 161
samabcde Avatar answered Jun 06 '26 23:06

samabcde



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!