For context, I’m using Resilience4j to handle exceptions and invoke retries. This is done through RetryService.
In this function, callRunnableWithRetry()
there are two params. 1st is a string, the 2nd is a Runnable. I have designed the function like this to make it modular so that anyone can process whatever they code block want to.
@Test
void testRetry(){
RetryService retryServiceSpy = spy(retryService);
ObjectA obj = new ObjectA("test-value");
Runnable runnable = mock(Runnable.class);
doThrow(new RuntimeException("Simulated exception"))
.doThrow(new RuntimeException("Simulated exception"))
.doNothing()
.when(runnable).run();
doAnswer(invocation -> {
String configName = invocation.getArgument(0);
Runnable actualRunnable = invocation.getArgument(1);
Retry retry = retryUtilSpy.getRetry(configName);
CheckedRunnable checkedRunnable = Retry.decorateCheckedRunnable(retry, actualRunnable::run);
try {
checkedRunnable.run();
} catch (Exception ignored) {
log.error("error: {}", ignored)
}
return null;
}).when(retryUtilService).callRunnableWithRetry(anyString(), any(Runnable.class));
serviceA.getData(obj);
verify(runnable, times(3)).run();
}
Issue
As you can see i’m creating a mock that throws exceptions twice then does nothing. I need this so i can trigger my retry functionality on my the runnable that I pass to callRunnableWithRetry()
through resilience4j.
When i use verify to see if runnable.run() was actually called 3x Mockito gives me this error.
Wanted but not invoked:
runnable.run();
-> at [redacted]
Actually, there were zero interactions with this mock.
So i’ve also tried to create a spy of my Runnable then inject it to my doAnswer(), so that way I can make sure that my mocked Runnable is actually being applied in my serviceA.getData()
call
Context
If curious this is RetryService
@Service
@Sl4j
public class RetryService {
private static final String RETRY_LOG_MESSAGE = "%s %s from service: %s" +
"nattempts made: %s" +
"nexception:n```%s```";
public Retry getRetry(String retryName) {
//process to acquire Reslience4j retry object
...
return retry;
}
private void handleRetryEvents(Retry retry, String action) {
retry.getEventPublisher()
.onSuccess(event -> logEvent(action, false, event))
.onRetry(event -> logEvent(action, false, event))
.onError(event -> logEvent(action, true, event));
}
private void logEvent(String action, boolean isAlert, RetryEvent event) {
//maps data to RETRY_LOG_MESSAGE string
...
}
public CheckedRunnable callRunnableWithRetry(String configName, Runnable runnableFunc) {
Retry retry = getRetry(configName);
handleRetryEvents(retry, "read");
return decorateCheckedRunnable(retry, () -> runnableFunc.run());
}
}
And this is how my implementation code looks in ServiceA
@Service
@Sl4j
public class ServiceA {
private final RetryService retryService;
public ServiceA(RetryService retryService){
this.retryService = retryService;
}
public void getData(ObjectA obj) {
try {
//processes data
...
retryService.callRunnableWithRetry(obj.getName(), () -> {
log.debug("Name: {}", obj.getName());
}).run();
} catch (Throwable e) {
log.error("error: {}", e);
}
}
}