如何在 spock 中测试 ListenableFuture 回调

How to test ListenableFuture Callbacks in spock

几天前我问了一个关于对 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
    }
}

我已经尝试了以下文章,但一无所获,我可能对这个工具的使用有误解。

更新:部分工作

我能够通过使用在回调中点击 onSuccessonFailure future.set(sendResult)future.setException(new Throwable()) 分别(感谢@GarryRussell 回答 )。但问题是验证 onSuccessonFailure 方法的行为。对于 示例 我有一个日志对象实体,我在其中保存状态(成功或失败),对此行为的断言始终 returns 为真。这是成功场景的更新测试代码。


    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 时,onSuccessonFailure 回调方法的右花括号上仍然有一个红色代码指示。

一般评论

除了我的评论之外,因为您似乎是测试自动化的初学者,尤其是模拟测试,一些一般性建议:

  • 测试主要不是质量检查工具,它只是一个理想的副作用。
  • 相反,它们是您应用程序的设计工具,尤其是在使用 TDD 时。 IE。编写测试可以帮助您重构代码以实现简单性、优雅性、可读性、可维护性、可测试性(您可能想阅读有关干净代码和软件工艺的内容):
    • 测试反馈到应用程序代码中,即如果很难测试某些东西,您应该重构代码。
    • 如果你有很好的测试覆盖率,你也可以无所畏惧地重构,也就是说,如果你的重构破坏了现有的应用程序逻辑,你的自动测试会立即检测到它,你可以在它变成大混乱之前修复一个小故障。
  • 一种典型的重构类型是通过将嵌套的逻辑层分解为分层的辅助方法,甚至分解为处理特定方面的特定 classes 来消除方法的复杂性。它使代码更容易理解,也更容易测试。
  • 熟悉依赖注入 (DI) 设计模式。一般原则称为控制反转 (IoC)。

话虽如此,我想提一下软件开发中导致有问题的应用程序设计和糟糕的可测试性的一种典型反模式是,如果 classes 和方法创建自己的内联依赖项而不是允许(甚至要求)用户注入它们。

所问问题的答案

你的情况就是一个很好的例子:你想验证你的 ListenableFutureCallback 回调挂钩是否按预期被调用,但你不能,因为该对象是在 sendMessage 方法中作为匿名创建的subclass 并分配给局部变量。本地 = 无法以简单的方式进行测试,并且没有像滥用日志服务来测试那些回调挂钩的副作用这样的肮脏技巧。试想一下,如果这些方法不再记录或仅基于特定的日志级别或调试条件会发生什么:测试将中断。

那么为什么不将回调实例的创建分解为特殊服务或至少分解为方法呢?该方法甚至不需要 public、受保护或包范围内就足够了——只是不是私有的,因为您不能模拟私有方法。

这是我给你的 MCVE。我通过直接控制台日志记录替换您的日志服务来消除一些复杂性,以证明您不需要在那里验证任何副作用。

package de.scrum_master.Whosebug.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.Whosebug.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(_)
  }
}

关于测试请注意:

  • 我们在 KafkaService 上使用 Spy,即包装原始实例的特殊类型的部分模拟。
  • 在这个间谍上,我们存根新方法 createCallback() 以便将模拟回调注入 class。这允许我们稍后验证是否按预期调用了 onSuccess(_)onFailure(_) 等交互。
  • 无需模拟或实例化任何 RecordMetadataTopicPartition

尽情享受吧! :-)


更新: 补充说明:

  • 间谍很管用,但是每当我使用间谍的时候我都有一种不安的感觉。可能是因为...
  • 将方法分解为受保护的辅助方法是使间谍能够存根方法或单独测试方法的一种简单方法。但是许多开发人员不赞成使方法可见(即使只是受保护而不是 public)(?),因为它使代码更易于测试。我不同意主要是因为正如我所说:测试是一种设计工具,更小、更集中的方法更容易理解、维护和重用。由于需要存根,辅助方法不能是私有的,有时不是很好。另一方面,受保护的辅助方法使我们能够在生产子程序中覆盖它class,因此还有一个与测试无关的优势。
  • 那么有什么选择呢?正如我上面所说,您可以将代码提取到一个集中的 extra class(内部静态 class 或单独的)而不是一个 extra 方法中。 class 可以单独进行单元测试,无需使用间谍即可进行模拟和注入。但是当然你需要公开一个接口来通过构造函数或 setter.
  • 注入协作者实例

没有所有开发人员都同意的完美解决方案。我给你看了一个我认为非常干净的,并提到了另一个。

First we need to create object for ListenableFuture, initialise RecordMetaData for topic and Mock SendResult.

为给定块中的成功案例添加此内容:

 ListenableFuture future = new SettableListenableFuture<>();
    RecordMetadata recordMetadata
    recordMetadata = new RecordMetadata(new TopicPartition("topic", 1), 1L, 0L, 0L, 0L, 0, 0);
    SendResult<String,String> sendResult = Mock(SendResult.class);
    sendResult.getRecordMetadata() >> recordMetadata;
    future.set(sendResult)

为给定块中的失败案例添加此内容:

Here we need to create object for ListenableFuture, intialize exception object.

    ListenableFuture future = new SettableListenableFuture<>();
    Throwable objthrowable = new Throwable()
    future.setException(objthrowable)