如何在 spock 中测试 ListenableFuture 回调 [英] How to test ListenableFuture Callbacks in spock

查看:23
本文介绍了如何在 spock 中测试 ListenableFuture 回调的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

几天前我问了一个关于从 kafka.send() 方法存根未来响应的问题.@kriegaex 此处回答并正确解释了这一点虽然我遇到了另一个问题,但我如何测试这个未来响应的 onSuccess 和 onFailure 回调.这是正在测试的代码.

I asked a question a few days ago regarding stubbing the future response from the kafka.send() method. this was answered and explained correctly by @kriegaex here Though I faced another issue, on how can i test the onSuccess and onFailure callbacks of this future response. here's the code under testing.

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
    }
}

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

I've tried this following articles and got me nowhere, I might be misunderstand to usage of this tool.

更新:部分工作

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

I was able to hit the onSuccess and onFailure on the callback by using future.set(sendResult) and future.setException(new Throwable()) respectively (thanks to @GarryRussell answer here). but the problem is verifying the behavior on the onSuccess and onFailure method. for example I have a log object entity where I save the status (success or failed), assertion on this behavior always returns true. here's the updated test code for the success scenario.


    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 回调方法的右花括号上仍然有红色代码指示.

one more thing that I observe is that when I run the code covarage, theres still a red code indication on the closing curly braces for onSuccess and onFailure callback methods.

推荐答案

一般评论

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

General comments

In addition to my comments and because you seem to be a beginner in test automation, especially mock testing, some general advice:

  • 测试不是主要的质量检查工具,它只是一种理想的副作用.
  • 相反,它们是您的应用程序的设计工具,尤其是在使用 TDD 时.IE.编写测试可以帮助您重构代码以实现简单、优雅、可读性、可维护性、可测试性(您可能想了解干净的代码和软件工艺):
    • 测试会反馈到应用程序代码中,也就是说,如果某些东西难以测试,您应该重构代码.
    • 如果您有良好的测试覆盖率,您还可以无畏地进行重构,即如果您的重构破坏了现有的应用程序逻辑,您的自动测试将立即检测到它,您可以在小故障变成大问题之前修复它.

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

    Having said that, I like to mention that one typical anti pattern in software development leading to problematic application design and bad testability is if classes and methods create their own dependencies inline instead of permitting (or even requiring) the user to inject them.

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

    Your situation is a good example: You want to verify that your ListenableFutureCallback callback hooks are being called as expected, but you cannot because that object is created inside the sendMessage method as an anonymous subclass and assigned to a local variable. Local = untestable in an easy way and without dirty tricks like abusing the log service to test a side effect of those callback hooks. Just imagine what would happen if the methods would not log anymore or only based on a specific log level or debug condition: The test would break.

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

    So why don't you factor out the callback instance creation into a special service or at least into a method? The method does not even need to be public, protected or package-scoped would suffice - just not private because you cannot mock private methods.

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

    Here is my MCVE for you. I removed some complexity by replacing your log service by direct console logging in order to demonstrate that you don't need to verify any side effects there.

    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,即一种特殊类型的部分模拟包装原始实例.
    • 在这个 spy 上,我们存根新方法 createCallback() 以便将模拟回调注入到类中.这允许我们稍后验证是否已按预期调用了诸如 onSuccess(_)onFailure(_) 之类的交互.
    • 无需模拟或实例化任何 RecordMetadataTopicPartition.
    • We are using a Spy on the KafkaService, i.e. a special type of partial mock wrapping an original instance.
    • On this spy we stub the new method createCallback() in order to inject a mock callback into the class. This allows us to verify later if interactions such as onSuccess(_) or onFailure(_) have been called on it as expected.
    • There is no need to mock or instantiate any of RecordMetadata or TopicPartition.

    享受吧!:-)

    更新:更多评论:

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

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

    There is no perfect solution all developers would agree on. I showed you one that I think is pretty much clean and mentioned another one.

    这篇关于如何在 spock 中测试 ListenableFuture 回调的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆