本文介绍了Spring AOP+AspectJ:@AfterReturning建议在嘲笑时错误执行(在实际执行之前)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在集成测试中,我@AfterReturning的建议被错误执行,而在测试中我模拟它抛出TimeoutException,传递给方面的arg为空。

我的建议:

    @AfterReturning("execution(* xxxxxx" +
            "OrderKafkaProducerService.sendOrderPaidMessage(..)) && " +
            "args(order)")
    public void orderComplete(CheckoutOrder order) { // order is null when debugging
        metricService.orderPaidKafkaSent();
        log.trace("Counter inc: order paid kafka"); // this line of log is shown in console
        metricService.orderCompleted();
        log.trace("Order complete! {}", order.getId()); // this line is not, because NPE
    }

我的测试:

// mocking
doThrow(new ServiceException(new TimeoutException("A timeout occurred"), FAILED_PRODUCING_ORDER_MESSAGE))
    .when(orderKafkaProducerService).sendOrderPaidMessage(any()); // this is where advice is executed, which is wrong

...
// when
(API call with RestAssured, launch a real HTTP call to endpoint; service is called during this process)

// then
verify(orderKafkaProducerService).sendOrderPaidMessage(any(CheckoutOrder.class)); // it should be called
verify(metricService, never()).orderCompleted(); // but we are throwing, not returning, we should not see this advice executed

由于NPE(订单为空),此测试失败。

在调试中,我发现在我模仿的时候,我已经执行了通知,并且在这一点上,any()还没有值,是空的,所以NPE。但我认为,这个建议不应该在嘲笑的同时执行。我如何在测试时避免这种情况??这对我来说太荒谬了。

推荐答案

目前,Spring测试支持没有显式处理注入的模拟或间谍(它是通过Mockito的代理子类)以后可能实际上是AOP目标的情况(即,被代理,因此通过CGLIB再次子类化)。

Spring、Spring Boot和Mockito有几个与此主题相关的错误标签。目前还没有人对此采取任何行动。我确实理解为什么Mockito的维护者不会在他们的代码库中包含特定于Spring的东西,但是我不理解为什么Spring的人不改进他们的测试工具。

实际上,在调试failing test和检查kafkaService时,您会发现以下事实:

  1. kafkaService.getClass()com.example.template.services.KafkaService$MockitoMock$92961867$$EnhancerBySpringCGLIB$$8fc4fe95
  2. kafkaService.getClass().getSuperclass()com.example.template.services.KafkaService$MockitoMock$92961867
  3. kafkaService.getClass().getSuperclass().getSuperclass()class com.example.template.services.KafkaService

换言之:

  1. kafkaService是CGLIB Spring AOP代理。
  2. AOP代理包装Mockito间谍(可能是ByteBuddy代理)。
  3. Mockito间谍包装原始对象。
此外,更改包装顺序以使Mockito侦察最外层的对象将不起作用,因为CGLIB故意使其覆盖方法成为最终方法,即您不能再次扩展和覆盖它们。如果Mockito也有同样的限制,分层包装将根本不起作用。

不管怎样,你能做什么?

  • 您要么使用this tutorial
  • 中所述的复杂方法
  • 或者您选择廉价的解决方案,通过AopTestUtils.getTargetObject(Object)显式地解包AOP代理。您可以安全地调用此方法,因为如果传递的候选对象不是Spring代理(内部易于识别,因为它实现了Advised接口,该接口也提供对目标对象的访问),它只是再次返回传递的对象。

在您的情况下,后一种解决方案如下所示:

@Test
void shouldCompleteHappyPath() {
    // fetch spy bean by unwrapping the AOP proxy, if any
    KafkaService kafkaServiceSpy = AopTestUtils.getTargetObject(kafkaService);
    // given mocked
    doNothing().when(kafkaServiceSpy).send(ArgumentMatchers.any());
    // when (real request)
    testClient.create().expectStatus().isOk();
    // then
    verify(kafkaServiceSpy).send(ArgumentMatchers.any());
    verify(metricService).incKafka();
}
这将导致when(kafkaServiceSpy).send(ArgumentMatchers.any())不再触发方面建议,因为kafkaServiceSpy不再是AOP代理。但是,自动连接的BeankafkaService仍然是,因此AOP会像预期的那样被触发,但在记录模拟交互时不再意外地触发。

实际上,对于验证,您甚至可以使用kafkaService代替间谍,只在记录您稍后要验证的交互时才打开间谍:

@Test
void shouldCompleteHappyPath() {
    // given mocked
    doNothing()
      .when(
        // fetch spy bean by unwrapping the AOP proxy, if any
        AopTestUtils.<KafkaService>getTargetObject(kafkaService)
      )
      .send(ArgumentMatchers.any());
    // when(real request)
    testClient.create().expectStatus().isOk();
    // then
    verify(kafkaService).send(ArgumentMatchers.any());
    verify(metricService).incKafka();
}

附言:如果没有您的MCVE,我永远不可能调试这个程序并找出到底是怎么回事。这再次证明,包括MCVE在内的提问是您可以为自己做的最好的事情,因为它可以帮助您获得问题的答案,否则这些问题可能无法得到回答。


更新:我在类似的闭合问题Spring Boot #6871下提到了这个问题后,其中一个维护人员自己创建了Spring Boot #22281,在这里专门解决了您的问题。您可能需要查看新问题,以确定是否/何时可以修复该问题。

这篇关于Spring AOP+AspectJ:@AfterReturning建议在嘲笑时错误执行(在实际执行之前)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

11-03 09:23