В контексте я использую 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();
}
Проблема
Как вы можете видеть, я создаю макет, который дважды выдает исключения, а затем ничего не делает. Мне это нужно, чтобы я мог активировать функцию повторной попытки на моем исполняемом объекте, который я передаю callRunnableWithRetry()
через resilience4j.
Когда я использую проверку, чтобы узнать, действительно ли 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);
}
}
}
Короче говоря: вы никуда не «инъецируете» свой высмеянный Runnable. Вы инициализируете макет, заглушаете метод, а затем забываете об этом экземпляре (не используя его больше нигде).
- retryUtilService и retryService одинаковы. Я обновлю код, чтобы он оставался целостным и исключал путаницу. - была версия этого кода, в которой я вводил свой издеваемый runnable в callRunnableWithRetry(), но я все еще вижу, что runnable вызывался 0 раз. Я обновлю код, чтобы показать это
Как поясняется в комментариях, вы ничего не делаете со своим mockRunnable
. Давайте посмотрим на это подробно.
Runnable mockRunnable = mock(Runnable.class);
// ...
doAnswer(invocation -> {
// irrelevant ...
return null;
}).when(retryService).callRunnableWithRetry("test", mockRunnable);
Вышеупомянутое гласит: если (или когда) метод callRunnableWithRetry
на retryService
вызывается с аргументами "test"
и точным экземпляром mockRunnable
(Runnable
не реализует equals()
, а фиктивные экземпляры Mockito всегда сравниваются только через идентификатор/ссылку), затем вызовите блок кода ответа .
Заглушка метода таким способом не вызывает метод и не заменяет фактические аргументы метода.
Как на самом деле называется callRunnableWithRetry
в производственном коде вашей SUT 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 этим макетом и не делает экземпляр доступным для ваших объектов, если вы явно не сделаете это в своем коде. Подобные варианты этой проблемы и возможные решения описаны в разделе Почему мои имитируемые методы не вызываются при выполнении модульного теста?
Одно из возможных решений:
doAnswer(invocation -> {
// ...
return null;
}).when(retryService).callRunnableWithRetry(eq("test"), any(Runnable.class));
большое спасибо за разъяснение. Это действительно собирает воедино все мое замешательство. Я хотел бы проверить, работает ли механизм повтора в ServiceA. У меня есть тест, работающий для RetryService. Я также переименую эту функцию в своем коде.
@frlzjosh, если вы действительно хотите протестировать механизм повтора в ServiceA, тогда вы должны использовать настоящий механизм повтора, а не издеваться над ним. Если вы издеваетесь над этим, вы не тестируете его, вы тестируете двойника.
В чем разница между
retryUtilService
иretryService
? Как вы создаете экземплярserviceA
в своем тесте? При этом вы никогда ничего не делаете со своим экземпляромrunnable
— он не подключен к вашему сервису, по крайней мере, в той части кода, которую вы показали. Я думаю, что в конечном итоге это та же проблема, что описана в stackoverflow.com/q/74027324/112968: инициализация и заглушка макета не делает его волшебным образом доступным в других классах, вы должны убедиться, что ваш SUT действительно взаимодействует с макетным экземпляром.