对于上下文,我使用 Resilience4j 来处理异常并调用重试。这是通过 RetryService 完成的。
在此函数中,
callRunnableWithRetry()
有两个参数。第一个是字符串,第二个是 Runnable。我设计了这样的函数,使其模块化,以便任何人都可以处理他们想要处理的任何代码块。
@Test
void testRetry(){
RetryService retryServiceSpy = spy(retryService);
ObjectA obj = new ObjectA("test-value");
Runnable mockRunnable = mock(Runnable.class);
doThrow(new RuntimeException("Simulated exception"))
.doThrow(new RuntimeException("Simulated exception"))
.doNothing()
.when(mockRunnable).run();
doAnswer(invocation -> {
String configName = invocation.getArgument(0);
Runnable actualRunnable = invocation.getArgument(1);
Retry retry = retryServiceSpy.getRetry(configName);
CheckedRunnable checkedRunnable = Retry.decorateCheckedRunnable(retry, actualRunnable::run);
try {
checkedRunnable.run();
} catch (Exception ignored) {
log.error("error: {}", ignored)
}
return null;
}).when(retryService).callRunnableWithRetry("test", mockRunnable);
serviceA.getData(obj);
verify(mockRunnable, times(3)).run();
}
问题
正如你所看到的,我正在创建一个模拟,它抛出异常两次然后什么都不做。我需要这个,这样我就可以在我通过resilience4j传递给
callRunnableWithRetry()
的可运行对象上触发我的重试功能。
当我使用 verify 来查看 runnable.run() 是否实际上被调用时 3x Mockito 给了我这个错误。
Wanted but not invoked:
runnable.run();
-> at [redacted]
Actually, there were zero interactions with this mock.
所以我还尝试创建我的 Runnable 的间谍,然后将其注入到我的 doAnswer() 中,这样我就可以确保我的模拟 Runnable 实际上被应用在我的
serviceA.getData()
调用中,但即使这样也没有工作;但如果有人可以让它工作,请分享。
背景
如果好奇的话,这是 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());
}
}
这就是我的实现代码在 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);
}
}
}
正如评论中所解释的,您没有对您的
mockRunnable
做任何事情。让我们详细看看吧。
doAnswer(invocation -> {
// irrelevant
return null;
}).when(retryService).callRunnableWithRetry("test", mockRunnable);
上面的状态:callRunnableWithRetry
上的if(或
when)方法
retryService
被调用,参数为"test"
和确切的mockRunnable
实例(Runnable
不实现equals()
和Mockito模拟)实例始终仅通过身份/引用进行比较),然后调用答案代码块。
以这种方式存根方法不会调用该方法,也不会替换该方法的实际参数。
您的 SUT
callRunnableWithRetry
的生产代码实际上如何调用 ServiceA
?
retryService.callRunnableWithRetry(obj.getName(), () -> {
log.debug("Name: {}", obj.getName());
}).run();
它的第二个参数是
() -> { log.debug("Name: {}", obj.getName()); }
,这很明显不是你的mockRunnable
实例,所以存根答案不匹配,因此没有被执行。
我也不太清楚为什么你在存根答案中重新实现
RetryService
的生产逻辑。您想测试哪个课程?如果你想测试RetryService
,请在测试中去掉ServiceA
并直接测试RetryService。如果你想测试 ServiceA
,只需验证 callRunnableWithRetry
方法是否已被调用(也许它的返回值已运行?)。请注意,方法名称 callRunnableWithRetry
本身非常具有误导性,因为该方法实际上并不调用可运行对象 - 它返回一个新的可运行对象,需要调用该新可运行对象才能执行原始可运行对象。
创建
Runnable
类的 Mockito 模拟实例不会神奇地用此模拟替换所有 Runnable 实例,也不会使该实例可供您的对象使用,除非您在代码中明确这样做。此问题的类似变体和可能的解决方案在以下内容中进行了解释:为什么在执行单元测试时未调用我的模拟方法?