Как проверить ListenableFuture Callbacks в споке - PullRequest
0 голосов
/ 10 апреля 2020

Несколько дней назад я задал вопрос go о том, как заглушить будущий ответ от метода kafka.send (). на это ответили и правильно объяснили @kriegaex здесь Хотя я столкнулся с другой проблемой, касающейся того, как я могу проверить обратные вызовы onSuccess и onFailure этого будущего ответа. вот тестируемый код.

import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;

public class KakfaService {

    private final KafkaTemplate<String, String> kafkaTemplate;
    private final LogService logService;

    public KakfaService(KafkaTemplate kafkaTemplate, LogService logService){
        this.kafkaTemplate = kafkaTemplate;
        this.logService = logService;
    }

    public void sendMessage(String topicName, String message) {
        ListenableFuture<SendResult<String, String>> future = kafkaTemplate.send(topicName, message);
        future.addCallback(new ListenableFutureCallback<SendResult<String, String>>() {

            @Override
            public void onSuccess(SendResult<String, String> result) {
              LogDto logDto = new LogDto();
              logDto.setStatus(StatusEnum.SUCCESS);
              logService.create(logDto)
            }
            @Override
            public void onFailure(Throwable ex) {
              LogDto logDto = new LogDto();
              logDto.setStatus(StatusEnum.FAILED);
              logService.create(logDto)
            }
        });
    }
}

и вот тестовый код

import com…….KafkaService
import com…….LogService
import org.apache.kafka.clients.producer.RecordMetadata
import org.apache.kafka.common.TopicPartition
import org.springframework.kafka.core.KafkaTemplate
import org.springframework.kafka.support.SendResult
import org.springframework.util.concurrent.ListenableFuture
import org.springframework.util.concurrent.ListenableFutureCallback
import org.springframework.util.concurrent.SettableListenableFuture
import spock.lang.Specification

public class kafaServiceTest extends Specification {

    private KafkaTemplate<String, String> kafkaTemplate;
    private KafkaService kafaService;
    private SendResult<String, String> sendResult;
    private SettableListenableFuture<SendResult<?, ?>> future;
    private RecordMetadata recordMetadata
    private String topicName
    private String message


    def setup() {
        topicName = "test.topic"
        message = "test message"
        sendResult = Mock(SendResult.class);
        future = new SettableListenableFuture<>();
        recordMetadata = new RecordMetadata(new TopicPartition(topicName, 1), 1L, 0L, 0L, 0L, 0, 0);

        kafkaTemplate = Mock(KafkaTemplate.class)

        logService = Mock(LogService.class)
        kafkaSservice = new KafkaSservice(kafkaTemplate, logService);
    }

    def "Test success send message method"() {
        given:
        sendResult.getRecordMetadata() >> recordMetadata
        ListenableFutureCallback listenableFutureCallback = Mock(ListenableFutureCallback.class);
        listenableFutureCallback.onFailure(Mock(Throwable.class))
        future.addCallback(listenableFutureCallback)

        when:
        kafkaService.sendMessage(topicName, message)

        then:
        1 * kafkaTemplate.send(_ as String, _ as String) >> future
        // test success of failed callbacks
    }
}

Я пробовал следующие статьи и ни к чему не привел, возможно, я неправильно понял использование этого инструмента .

ОБНОВЛЕНИЕ: УЧАСТИЕ В РАБОТЕ

Мне удалось поразить onSuccess и onFailure на обратный вызов с использованием future.set(sendResult) и future.setException(new Throwable()) соответственно (благодаря @GarryRussell ответу здесь ). но проблема заключается в проверке поведения метода onSuccess и onFailure. для пример У меня есть объект журнала, в котором я сохраняю статус (успешный или неудачный), утверждение об этом поведении всегда возвращает true. Вот обновленный тестовый код для сценария успеха.


    def "Test success send message method"() {
        given:
        sendResult.getRecordMetadata() >> recordMetadata
        future.set(sendResult)

        when:
        kafkaService.sendMessage(topicName, message)

        then:
        1 * kafkaTemplate.send(_ as String, _ as String) >> future
        1 * logService.create(_) >> {arguments ->
            final LogDto logDto = arguments.get(0)
            // this assert below should fail
            assert logDto.getStatus() == LogStatus.FAILED 
        }
    }

Еще одна вещь, которую я наблюдаю, заключается в том, что когда я запускаю код covarage, на закрывающих фигурных скобках для onSuccess все еще остается красная индикация кода onFailure методы обратного вызова.

1 Ответ

0 голосов
/ 10 апреля 2020

Общие комментарии

В дополнение к моим комментариям, а также потому, что вы, кажется, новичок в автоматизации тестирования, особенно в фиктивном тестировании, некоторые общие советы:

  • Тесты в основном не являются инструмент проверки качества, это только желаемый побочный эффект.
  • Вместо этого они являются инструментом дизайна для вашего приложения, особенно при использовании TDD. Т.е. написание тестов помогает вам реорганизовать ваш код для простоты, элегантности, удобочитаемости, удобства сопровождения, тестируемости (вы можете прочитать о чистом коде и мастерстве программного обеспечения):
    • Тесты возвращаются в код приложения, т.е. Трудно что-то протестировать, вам следует провести рефакторинг кода.
    • Если у вас хорошее тестовое покрытие, вы также можете безбоязненно рефакторировать, т. е. если ваш рефакторинг нарушает логи существующего приложения c, ваши автоматические c тесты будут немедленно обнаружите его, и вы можете исправить небольшой сбой, прежде чем он станет большим беспорядком.
  • Один из типичных типов рефакторинга - это удаление сложности из методов путем выделения вложенных слоев логики c в многослойные вспомогательные методы или даже в определенные c классы, обеспечивающие определенный аспект. Это облегчает понимание кода и его тестирование.
  • Ознакомьтесь с шаблоном проектирования Dependency Injection (DI). Общий принцип называется Inversion of Control (Io C).

Сказав это, я хотел бы упомянуть, что один типичный анти-паттерн в разработке программного обеспечения ведет к проблемным c проектам приложений и плохим Тестируемость - это если классы и методы создают свои собственные встроенные зависимости вместо того, чтобы разрешать (или даже требовать) пользователю вводить их.

Ответ на заданный вопрос

Ваша ситуация является хорошим примером: вы хотите проверить, что ваши ListenableFutureCallback перехватчики обратного вызова вызываются, как и ожидалось, но вы не можете, потому что этот объект создан внутри метода sendMessage как анонимный подкласс и назначен локальной переменной. Local = не тестируется простым способом и без грязных уловок, таких как злоупотребление службой журналов для проверки побочного эффекта этих перехватчиков обратного вызова. Только представьте, что произойдет, если методы больше не будут регистрироваться или будут основаны только на заданном c уровне журнала или условии отладки: тест прервется.

Так почему бы вам не выделить создание экземпляра обратного вызова в специальный сервис или хотя бы метод? Этот метод даже не нужно публиковать c, достаточно защищенного или пакетного уровня - просто не частного, потому что вы не можете высмеивать частные методы.

Вот мой MCVE для вас. Я убрал некоторые сложности, заменив вашу службу журналов на прямую консольную регистрацию, чтобы продемонстрировать, что вам не нужно проверять какие-либо побочные эффекты.

package de.scrum_master.stackoverflow.q61100974;

import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;

public class KafkaService {
  private KafkaTemplate<String, String> kafkaTemplate;

  public KafkaService(KafkaTemplate kafkaTemplate) {
    this.kafkaTemplate = kafkaTemplate;
  }

  public void sendMessage(String topicName, String message) {
    ListenableFuture<SendResult<String, String>> future = kafkaTemplate.send(topicName, message);
    future.addCallback(createCallback());
  }

  protected ListenableFutureCallback<SendResult<String, String>> createCallback() {
    return new ListenableFutureCallback<SendResult<String, String>>() {
      @Override
      public void onSuccess(SendResult<String, String> result) {
        System.out.print("Success -> " + result);
      }

      @Override
      public void onFailure(Throwable ex) {
        System.out.print("Failed -> " + ex);
      }
    };
  }
}
package de.scrum_master.stackoverflow.q61100974

import org.springframework.kafka.core.KafkaTemplate
import org.springframework.kafka.support.SendResult
import org.springframework.util.concurrent.ListenableFuture
import org.springframework.util.concurrent.ListenableFutureCallback
import org.springframework.util.concurrent.SettableListenableFuture
import spock.lang.Specification

class KafkaServiceTest extends Specification {

  KafkaTemplate<String, String> kafkaTemplate = Mock()
  ListenableFutureCallback callback = Mock()

  // Inject mock template into spy (wrapping the real service) so we can verify interactions on it later
  KafkaService kafkaService = Spy(constructorArgs: [kafkaTemplate]) {
    // Make newly created helper method return mock callback so we can verify interactions on it later
    createCallback() >> callback
  }

  SendResult<String, String> sendResult = Stub()
  String topicName = "test.topic"
  String message = "test message"
  ListenableFuture<SendResult<String, String>> future = new SettableListenableFuture<>()

  def "sending message succeeds"() {
    given:
    future.set(sendResult)

    when:
    kafkaService.sendMessage(topicName, message)

    then:
    1 * kafkaTemplate.send(topicName, message) >> future
    1 * callback.onSuccess(_)
  }

  def "sending message fails"() {
    given:
    future.setException(new Exception("uh-oh"))

    when:
    kafkaService.sendMessage(topicName, message)

    then:
    1 * kafkaTemplate.send(topicName, message) >> future
    1 * callback.onFailure(_)
  }
}

Пожалуйста, обратите внимание в отношении теста:

  • Мы используем Spy на KafkaService, то есть специальный тип частичной имитации обертывания исходного экземпляра.
  • На этом шпионе мы заглушаем новый метод createCallback() для того, чтобы ввести ложный обратный вызов в класс. Это позволяет нам позже проверить, были ли вызваны такие взаимодействия, как onSuccess(_) или onFailure(_), как и ожидалось.
  • Нет необходимости высмеивать или создавать какие-либо из RecordMetadata или TopicPartition.

Наслаждайтесь! : -)


Обновление: Еще несколько замечаний:

  • Шпионы работают, но всякий раз, когда я пользуюсь шпионом, у меня возникает чувство неловкости. Возможно, потому что ...
  • выделение методов в защищенные вспомогательные методы - это простой способ дать шпиону возможность заглушить метод или протестировать метод отдельно. Но многие разработчики недовольны практикой делать методы видимыми (даже если они защищены и не публикуются c) только (?), Потому что это облегчает тестирование кода. Я не согласен в основном потому, что, как я уже сказал, тесты - это инструмент проектирования, а более мелкие и более целенаправленные методы лучше понять, поддерживать и использовать повторно. То, что вспомогательный метод не может быть закрытым из-за необходимости заглушить его, иногда не очень приятно. С другой стороны, защищенный вспомогательный метод позволяет нам переопределить его в производственном подклассе, так что есть еще одно преимущество, не связанное с тестированием.
  • Так что же является альтернативой? Как я уже говорил выше, вы можете извлечь код в дополнительный класс (внутренний класс stati c или отдельный) вместо дополнительного метода. Этот класс может быть проверен отдельно и может быть подвергнут насмешке и инъекции без использования шпиона. Но тогда, конечно, вам нужно предоставить интерфейс для внедрения экземпляра соавтора через конструктор или установщик.

Не существует идеального решения, с которым согласились бы все разработчики. Я показал вам один, который я считаю довольно чистым, и упомянул еще один.

...