Mockito:使用函数参数调用验证方法

问题描述:

我有一个简单的场景,其中尝试验证某个方法被调用时的某些行为(即某个方法是使用给定参数调用的,此场景中的函数指针)。下面是我的课:Mockito:使用函数参数调用验证方法

@SpringBootApplication 
public class Application { 

    public static void main(String[] args) { 
     ConfigurableApplicationContext context = SpringApplication.run(Application.class, args); 

     AppBootStrapper bootStrapper = context.getBean(AppBootStrapper.class); 
     bootStrapper.start(); 
    } 
} 

@Component 
public class AppBootStrapper { 
    private NetworkScanner networkScanner; 
    private PacketConsumer packetConsumer; 

    public AppBootStrapper(NetworkScanner networkScanner, PacketConsumer packetConsumer) { 
     this.networkScanner = networkScanner; 
     this.packetConsumer = packetConsumer; 
    } 

    public void start() { 
     networkScanner.addConsumer(packetConsumer::consumePacket); 
     networkScanner.startScan(); 
    } 
} 

@Component 
public class NetworkScanner { 
    private List<Consumer<String>> consumers = new ArrayList<>(); 

    public void startScan(){ 
     Executors.newSingleThreadExecutor().submit(() -> { 
      while(true) { 
       // do some scanning and get/parse packets 
       consumers.forEach(consumer -> consumer.accept("Package Data")); 
      } 
     }); 
    } 

    public void addConsumer(Consumer<String> consumer) { 
     this.consumers.add(consumer); 
    } 
} 

@Component 
public class PacketConsumer { 
    public void consumePacket(String packet) { 
     System.out.println("Packet received: " + packet); 
    } 
} 

@RunWith(JUnit4.class) 
public class AppBootStrapperTest { 

    @Test 
    public void start() throws Exception { 
     NetworkScanner networkScanner = mock(NetworkScanner.class); 
     PacketConsumer packetConsumer = mock(PacketConsumer.class); 
     AppBootStrapper appBootStrapper = new AppBootStrapper(networkScanner, packetConsumer); 

     appBootStrapper.start(); 

     verify(networkScanner).addConsumer(packetConsumer::consumePacket); 
     verify(networkScanner, times(1)).startScan(); 
    } 
} 

我想验证引导程序并事实上通过注册数据包的消费者做正确的设置(有可能是后来注册的其他用户,但是这一次是强制性的),然后叫startScan。当我执行测试用例时,我收到以下错误信息:

Argument(s) are different! Wanted: 
networkScanner bean.addConsumer(
    com.spring.starter.AppBootStrapperTest$$Lambda$8/[email protected] 
); 
-> at com.spring.starter.AppBootStrapperTest.start(AppBootStrapperTest.java:24) 
Actual invocation has different arguments: 
networkScanner bean.addConsumer(
    com.spring.starter.AppBootStrapper$$Lambda$7/[email protected] 
); 
-> at com.spring.starter.AppBootStrapper.start(AppBootStrapper.java:12) 

从例外情况来看,显然函数指针并不相同。

我接近这个正确的方式吗?有什么基本的我失踪?我四处游玩,让一位消费者注入PacketConsumer,看看它是否与众不同,但这是可以的,但我知道这当然不是正确的选择。

任何帮助,对此的观点将不胜感激。

Java没有任何“函数指针”的概念;当你看到:

networkScanner.addConsumer(packetConsumer::consumePacket); 

什么的Java编译实际上是(相当于):

networkScanner.addConsumer(new Consumer<String>() { 
    @Override void accept(String packet) { 
    packetConsumer.consumePacket(packet); 
    } 
}); 

这个匿名内部类碰巧被称为AppBootStrapper$$Lambda$7。因为它没有(也不应该)定义一个equals方法,所以它永远不会等于编译器在测试中生成的匿名内部类,它恰好被称为AppBootStrapperTest$$Lambda$8。这与方法体是相同的事实无关,并且以相同方法参考的相同方式构建。

如果您在测试中明确生成消费者并将其保存为static final Consumer<String>字段,那么您可以在测试中传递该参考并进行比较;在这一点上,参考平等应该成立。这应该适用于lambda表达式或方法引用。

更适合的测试可能会verify(packetConsumer, atLeastOnce()).consumePacket(...),因为lambda的内容是一个实现细节,你真的更关心你的组件如何与其他组件协作。此处的抽象级别应该在consumePacket级别,而不是addConsumer级别。

请参阅this SO question上的评论和回答。

+0

谢谢@Jeff Bowman。很好地解释。我喜欢在消费者层面进行验证的想法。 –