Пытаюсь внедрить мой издевательский Runnable в мой сервисный вызов

В контексте я использую 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);
      }  
    }
}

В чем разница между retryUtilService и retryService? Как вы создаете экземпляр serviceA в своем тесте? При этом вы никогда ничего не делаете со своим экземпляром runnable — он не подключен к вашему сервису, по крайней мере, в той части кода, которую вы показали. Я думаю, что в конечном итоге это та же проблема, что описана в stackoverflow.com/q/74027324/112968: инициализация и заглушка макета не делает его волшебным образом доступным в других классах, вы должны убедиться, что ваш SUT действительно взаимодействует с макетным экземпляром.

knittl 13.07.2024 16:48

Короче говоря: вы никуда не «инъецируете» свой высмеянный Runnable. Вы инициализируете макет, заглушаете метод, а затем забываете об этом экземпляре (не используя его больше нигде).

knittl 15.07.2024 09:18

- retryUtilService и retryService одинаковы. Я обновлю код, чтобы он оставался целостным и исключал путаницу. - была версия этого кода, в которой я вводил свой издеваемый runnable в callRunnableWithRetry(), но я все еще вижу, что runnable вызывался 0 раз. Я обновлю код, чтобы показать это

frlzjosh 15.07.2024 17:25
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
3
93
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Как поясняется в комментариях, вы ничего не делаете со своим 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 18.07.2024 19:55

@frlzjosh, если вы действительно хотите протестировать механизм повтора в ServiceA, тогда вы должны использовать настоящий механизм повтора, а не издеваться над ним. Если вы издеваетесь над этим, вы не тестируете его, вы тестируете двойника.

knittl 21.07.2024 20:10

Другие вопросы по теме