【问题标题】:How to test ListenableFuture Callbacks in spock如何在 spock 中测试 ListenableFuture 回调
【发布时间】:2020-07-22 18:11:54
【问题描述】:

几天前我问了一个问题,关于从 kafka.send() 方法中存根未来的响应。 @kriegaex here 正确回答并解释了这一点 虽然我遇到了另一个问题,但我如何测试这个未来响应的 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 回答 here)。但问题是验证onSuccessonFailure 方法的行为。对于示例,我有一个日志对象实体,我在其中保存状态(成功或失败),对此行为的断言始终返回 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 
        }
    }

我观察到的另一件事是,当我运行代码协变时,onSuccessonFailure 回调方法的右花括号上仍然有一个红色代码指示。

【问题讨论】:

  • 您似乎没有在任何地方注入您的日志服务模拟。因此应用程序与它没有连接,因此无法调用它。您的示例服务类也不包含使用日志服务的任何线索。那么服务在哪里发挥作用呢?就像我在上一个问题中解释的那样,您没有提供MCVE,所以我无法重现您的问题。有很多代码,好吧。但是具有讽刺意味的是,您遇到问题的部分丢失了。如果您愿意更新问题并通知我,我会再看一下。 :-)
  • 同样在您的第二个变体中,kafkaStreamGateway 突然来自哪里?是否与前面示例中的kafkaService 相同? MCVE 意味着您创建一个独立的示例,在应用程序上下文之外单独运行它,以检查您是否忘记了任何依赖项,然后将其发布在此处。否则很容易忽略一些东西。作为替代方案,您可以将 MCVE 推送到 GitHub(包括 Maven 或 Gradle 构建配置),然后在此处发布链接。那会更好。
  • 嗨@kriegaex,我更新了这个问题,我在 setup() 方法中注入了logService。 kafkaStreamGateway 是即时测试的另一个包装函数。我现在从示例中删除它:)
  • 是的,但我仍然看不到日志服务或它在您的应用程序类中的使用方式。
  • 嗨@kriegaex 我更新了正在测试的类,抱歉,如果这里有很多更改,该类还有其他依赖项和逻辑,但我试图只提供主要功能。目前我正在测试它的主要功能是使用带有回调的 kafkatemplate 发送。

标签: unit-testing groovy spock spring-kafka


【解决方案1】:
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)

【讨论】:

  • 虽然这段代码 sn-p 可以解决问题,including an explanation 将帮助人们理解你的代码建议的原因。
【解决方案2】:

一般cmets

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

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

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

回答问题

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

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

这是我给你的 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(_)
  }
}

关于测试请注意:

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

享受吧! :-)


更新:更多评论:

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

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

【讨论】:

  • 嗨@kriegaex,我真的很感谢你在这里帮助我的时间!不仅您确实回答了我的 SO 问题,而且还给了我一些学习这些东西的建议和方向。顺便说一句,我的测试现在工作正常。我将以可测试的方式分离回调方法。再次谢谢你! :)
  • 嗨@kriegaex,我遇到了类似的问题,这个解决方案对我帮助很大,但我有一个问题:在 onSucess 内部,我调用了一个存储库,所以我可以保存一个对象。 ..按照您的示例,我可以测试是否调用了 onSuccess 方法,但无法验证是否调用了存储库...您有什么方法可以测试它吗?
  • 请提出一个新问题并提交一个完整的MCVE,我可以编译、运行和分析它。请不要缺少依赖项,不要不完整的代码 sn-ps。具体来说,我需要看看您如何注入或实例化存储库实例。如果您遵循我关于依赖注入的建议,那么模拟存储库并验证其交互应该很简单。请不要再在这里回答,除非可能是为了发布指向新的相关问题的链接。谢谢。
猜你喜欢
  • 2019-10-08
  • 2012-05-19
  • 1970-01-01
  • 2018-03-31
  • 2020-07-20
  • 1970-01-01
  • 2015-08-21
  • 2012-06-10
  • 1970-01-01
相关资源
最近更新 更多