PowerMockito verifyStatic: validating async call to static method

81 views Asked by At

I'm making call to an external system via a static method

MyExternalServiceAccessor.myMethod(param1, param2);

And so far I've been unit-testing above using PowerMockito's verifyStatic as follows

import static org.mockito.Matchers.eq;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.verifyStatic;
..

mockStatic(MyExternalServiceAccessor.class);

..

verifyStatic();
MyExternalService.myMethod(eq(arg1), eq(arg2));

I now want to make above call 'async' (best-effort call with fire-and-forget semantics, I don't care for the response); so taking reference from here I wrapped it inside CompletableFuture.runAsync(..) as follows

import java.util.concurrent.CompletableFuture;

CompletableFuture.runAsync(() -> {
    MyExternalServiceAccessor.myMethod(param1, param2);
});

But with this change as expected the unit-test has become flaky because due to the async invocation of MyExternalServiceAccessor.myMethod(..), sometimes validation fails with following error

java.lang.RuntimeException: Wanted but not invoked com.company.team.service.serviceutils.MyClass.myFunctionBeingUnitTested(
    null,
    null
);
Actually, there were zero interactions with this mock.

  • I'm aware that Mockito supports verification with timeout for non-static methods; is there a PowerMockito (or Mockito) equivalent of the same for static methods?
  • If not then what are the alternatives? Can I cleverly rewrite my code and tests in a way to keep it async while still having unit-tests pass everytime?
1

There are 1 answers

0
y2k-shubham On

I think I might've found a fix by using a non-static method call being made after my static method call as a 'proxy' for hinting that our static method call must've also happened

I was logging an error metric in case my static method call fails as follows

import java.util.concurrent.CompletableFuture;

CompletableFuture.runAsync(() -> {
    try {
        MyExternalServiceAccessor.myMethod(param1, param2);
    } catch (final Exception e) {
        // here metricLogger is injected into MyClass from outside so can be mocked for tests
        metricLogger.logCounter("MyExternalServiceAccessor.myMethod:failure", 1);
    }
});

Now in above code

  • metricLogger.logCounter(..) call is a non-static call for which we can leverage mockito's verification with timeout
  • and since it occurs after our static MyExternalServiceAccessor.myMethod(..) call so we can be sure that if logCounter call has happened then myMethod call must've also happened
    • although above statement is true only when we are unit-testing an error scenario where myMethod throws an exception

Therefore for an error scenario we write our test like this for waiting 100ms before validating that expected methods invoked

verify(mockMetricLogger, timeout(100).times(1)).logCounter(eq("MyExternalServiceAccessor.myMethod:failure"), eq(1));
verifyStatic();
MyExternalService.myMethod(eq(arg1), eq(arg2));

And for non-error scenario we can use the trick suggested in this thread to wait-and-verify that our mock was called 'zero' times, i.e., not called even after waiting

verify(mockMetricLogger, timeout(100).times(0)).logCounter(eq("MyExternalServiceAccessor.myMethod:failure"), eq(1));
verifyStatic();
MyExternalService.myMethod(eq(arg1), eq(arg2));

So far my tests have been passing, but since (a) they were not too flaky to begin with and (b) we're dealing with notorious async issues that can show up intermittently, I'm not certain if for non-error scenarios the flakiness has been completely eliminated or not.

(I can of course completely do away with the flakiness for example by also publishing a counter metric in success scenario and using that as a proxy, but don't want to do that just yet)

UPDATE-1

I also had to test out the scenario where this entire block of async code is never called (due to upstream conditional expressions preventing flow from reaching up till this point where async block is triggered)

For that I leveraged Mockito.never() as suggested in this thread

verify(mockMetricLogger, timeout(100).times(0)).logCounter(eq("MyExternalServiceAccessor.myMethod:failure"), eq(1));
verifyStatic(never());
MyExternalService.myMethod(eq(arg1), eq(arg2));