Unit testing 如何在spock中测试ListenableFuture回调

Unit testing 如何在spock中测试ListenableFuture回调,unit-testing,groovy,spock,spring-kafka,Unit Testing,Groovy,Spock,Spring Kafka,几天前我问了一个问题,关于从kafka.send()方法中删除将来的响应。@kriegaex正确地回答和解释了这一点 尽管我还面临另一个问题,关于如何测试这个未来响应的onSuccess和onFailure回调。下面是正在测试的代码 import org.springframework.kafka.core.KafkaTemplate; import org.springframework.kafka.support.SendResult; import org.springframework.

几天前我问了一个问题,关于从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)
            }
        });
    }
}

我观察到的另一件事是,当我运行code covarage时,
onSuccess
onFailure
回调方法的右大括号上仍然有一个红色代码指示

一般性意见 除了我的评论之外,由于您似乎是测试自动化(尤其是模拟测试)的初学者,因此,还有一些一般性建议:

  • 测试主要不是一个质量检查工具,这只是一个令人满意的副作用
  • 相反,它们是应用程序的设计工具,尤其是在使用TDD时。也就是说,编写测试可以帮助您重构代码,以实现简单性、优雅性、可读性、可维护性和可测试性(您可能想了解干净的代码和软件工艺):
    • 测试会反馈到应用程序代码中,也就是说,如果很难测试某些东西,您应该重构代码
    • 如果你有良好的测试覆盖率,你也可以无所畏惧地重构,也就是说,如果你的重构破坏了现有的应用程序逻辑,你的自动测试将立即检测到它,你可以在它变成一团乱麻之前修复一个小故障
  • 重构的一种典型类型是通过将嵌套的逻辑层分解成分层的辅助方法,甚至分解成处理特定方面的特定类,从而消除方法的复杂性。它使代码更容易理解,也更容易测试
  • 熟悉依赖注入(DI)设计模式。一般原理称为控制反转(IoC)
话虽如此,我还是想指出,软件开发中一个典型的反模式会导致有问题的应用程序设计和糟糕的可测试性,那就是如果类和方法内联创建它们自己的依赖项,而不是允许(甚至要求)用户注入它们

问题的答案 您的情况就是一个很好的例子:您希望验证是否按预期调用了
ListenableFutureCallback
回调挂钩,但您不能,因为该对象是在
sendMessage
方法中作为匿名子类创建的,并分配给局部变量。Local=untestable,使用简单的方法,并且没有诸如滥用日志服务来测试这些回调挂钩的副作用之类的肮脏伎俩。想象一下,如果这些方法不再记录日志,或者只基于特定的日志级别或调试条件,将会发生什么:测试将中断

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

这是我给你的MCVE。我用直接控制台日志取代了日志服务,从而消除了一些复杂性,以证明您不需要验证其中的任何副作用

package de.scrum_master.stackoverflow.q61100974;
导入org.springframework.kafka.core.KafkaTemplate;
导入org.springframework.kafka.support.SendResult;
导入org.springframework.util.concurrent.ListenableFuture;
导入org.springframework.util.concurrent.ListenableFutureCallback;
公共级卡夫卡服务{
私人卡夫卡模板卡夫卡模板;
公共卡夫卡服务(卡夫卡模板卡夫卡模板){
this.kafkaTemplate=kafkaTemplate;
}
公共无效发送消息(字符串主题名称,字符串消息){
ListenableFuture=kafkaTemplate.send(主题名,消息);
addCallback(createCallback());
}
受保护的ListenableFutureCallback createCallback(){
返回新的ListenableFutureCallback(){
@凌驾
成功时公共无效(SendResult结果){
系统输出打印(“成功->”+结果);
}
@凌驾
失效时的公共无效(可丢弃的ex){
系统输出打印(“失败->”+ex);
}
};
}
}
package de.scrum\u master.stackoverflow.q61100974
导入org.springframework.kafka.core.KafkaTemplate
导入org.springframework.kafka.support.SendResult
导入org.springframework.util.concurrent.ListenableFuture
导入org.springframework.util.concurrent.ListenableFutureCallback
导入org.springframework.util.concurrent.SettableListenableFuture
导入spock.lang.Specification
类KafkaServiceTest扩展了规范{
KafkaTemplate KafkaTemplate=Mock()
ListenableFutureCallback callback=Mock()
//将模拟模板注入spy(包装真正的服务),以便我们以后可以验证它上的交互
KafkaService KafkaService=Spy(构造函数args:[kafkaTemplate]){
//使新创建的helper方法返回模拟回调,以便我们以后可以验证它上的交互
createCallback()>>回调
}
SendResult SendResult=Stub()
String topicName=“test.topic”
String message=“测试消息”
ListenableFuture=新设置的ListenableFuture()
def“发送消息成功”(){
鉴于:
future.set(sendResult)
什么时候:
kafkaService.sendMessage(主题名,消息)
然后:
1*kafkaTemplate.send(主题名、消息)>>future
1*callback.onSuccess(\ux)
}
def“发送消息失败”(){
鉴于:
future.setException(新异常(“uh-oh”))
什么时候:
kafkaService.sendMessage(主题名,消息)
然后:
1*kafkaTemplate.send(主题名、消息)>>future
1*callback.onFailure(\ux)
}
}
关于测试,请注意:

  • 我们正在
    kafkase服务
    上使用
    Spy
    ,即一种特殊类型的部分模拟包装原始实例
  • 在这个间谍上,我们存根新方法
    createCallback()
    ,以便
    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
        }
    }
    
    
        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 
            }
        }