Mockito 使用 thenThrow 为 KafkaTemplate.send 抛出异常

Mockito throw exception for KafkaTemplate.send using thenThrow

我正在测试以下功能。

public boolean produceNumberOfPeople(NumberOfPeopleInPlaceDTO numberOfPeopleInPlaceDTO) {
    final ProducerRecord<Integer, Integer> record = new ProducerRecord<>(
        KafkaConfig.NUMBER_OF_PEOPLE_BY_PLACE_TOPIC,
        numberOfPeopleInPlaceDTO.getId(),
        numberOfPeopleInPlaceDTO.getNumberOfPeople());
    try {
        kafkaTemplate.send(record).get(2, TimeUnit.SECONDS);
        return true;
    }
    catch (ExecutionException | TimeoutException | InterruptedException e) {
        return false;
    }
}

这是测试代码。

@Test
public void produceNumberOfPeopleTest() throws InterruptedException, ExecutionException, TimeoutException {
    NumberOfPeopleInPlaceDTO numberOfPeopleInPlaceDTO = NumberOfPeopleInPlaceDTO.builder()
                .id(1)
                .numberOfPeople(10)
                .build();
    Mockito.when(kafkaTemplate.send(Mockito.any(ProducerRecord.class)))
        .thenReturn(listenableFuture);
    Mockito.when(listenableFuture.get(2,TimeUnit.SECONDS))
        .thenThrow(TimeoutException.class);
    Assert.assertFalse(placeService.produceNumberOfPeople(numberOfPeopleInPlaceDTO));
}

我定义了以下变量。

@Autowired
private PlaceService placeService;
@MockBean
private PlaceRepository placeRepository;
@MockBean
private KafkaTemplate<Integer, Integer> kafkaTemplate;
@MockBean
private ListenableFuture listenableFuture;

问题是 kafkaTemplate.send(record).get(2,TimeUnit.SECONDS) 没有抛出异常。所以测试一直失败。

有什么不足请指教。

我会建议创建失败的 ListenableFuture 对象而不是 Mock

SettableListenableFuture<SendResult<String, Object>> future = new SettableListenableFuture<>();
future.setException(new RuntimeException())

然后 return 模拟

 Mockito.when(kafkaTemplate.send(Mockito.any(ProducerRecord.class))).thenReturn(listenableFuture);

所以当 get 方法被调用时它抛出 ExecutionException

This method returns the value if it has been set via set(Object), throws an ExecutionException if an exception has been set via setException(Throwable), or throws a CancellationException if the future has been cancelled.

问题是 PlaceService 没有使用 KafkaTemplate 的模拟实例。所以我手动将模拟实例传递给 PlaceService 的构造函数。现在测试通过了。
这是新的测试代码。

@Test
public void produceNumberOfPeopleTest() throws InterruptedException, ExecutionException, TimeoutException {
    NumberOfPeopleInPlaceDTO numberOfPeopleInPlaceDTO = NumberOfPeopleInPlaceDTO.builder()
            .id(1)
            .numberOfPeople(10)
            .build();
    PlaceService testPlaceService = new PlaceServiceImpl(null,kafkaTemplate);
    SettableListenableFuture<SendResult<String, Object>> future = new SettableListenableFuture<>();
    future.setException(new RuntimeException());
    Mockito.when(kafkaTemplate.send(Mockito.any(ProducerRecord.class))).thenReturn(future);
    Assert.assertFalse(testPlaceService.produceNumberOfPeople(numberOfPeopleInPlaceDTO));
}

很高兴您通过将实例传递给构造函数解决了问题。

但是,我不会创建适当的构造函数并在测试方法本身中实例化 placeService,而是使用另一种方法。
作为最佳实践,建议使用特定的 setXXX 方法来传递实例,例如在您的情况下,在 PlaceService class 中您应该有这样的东西:

public void setListenableFuture(ListenableFuture listenableFuture) {
   this.listenableFuture = listenableFuture;
}

public void setKafkaTemplate(KafkaTemplate<Integer, Integer> kafkaTemplate) {
   this.kafkaTemplate = kafkaTemplate;
}

然后,您可以在您的测试方法中调用它们(在您的情况下 produceNumberOfPeopleTest),或者在特定的设置方法中调用它们更好,如下所示:

@Before
public void setUp() throws Exception {
   placeService.setListenableFuture(listenableFuture);
   placeService.setKafkaTemplate(kafkaTemplate);
}

通过这种方式,您可以将 Mock 对象和 placeService 作为您的测试成员 class,这样 JUnit 和 Spring Runner 将负责实例化这些对象并且将它们注入 placeService,然后您可以根据需要自定义每个测试方法的模拟行为。

根据我的经验,我发现这很有帮助,因为涉及的每个对象都各司其职。即使在测试实现和可维护性方面,您也不必在每个测试方法中重复相同的代码。例如,想一想如果在某个时候您必须更改该构造函数会发生什么情况,在这种情况下您将不得不更改您使用它的所有方法。你不觉得吗?