Spring AOP + AspectJ:@AfterReturning 建议在模拟时错误执行(在实际执行之前)

Spring AOP + AspectJ: @AfterReturning advice wrongly executed while mocking(before actual execution)

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

我的建议:

    @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(订单为空),此测试失败。

调试时发现在mocking的时候已经执行了advice,此时any()还没有值,为null,所以NPE.但我认为建议不应该在嘲笑时执行。我怎样才能在测试时避免这种情况?这对我来说很荒谬。

目前 Spring 测试支持没有明确处理注入的 mock 或 spy(通过 Mockito 的代理子类)可能实际上是稍后的 AOP 目标(即通过代理并因此再次子类化)的情况CGLIB).

Spring、Spring Boot 和 Mockito 有几个与此主题相关的错误票。还没有人对此做任何事情。我确实理解为什么 Mockito 维护者不会将 Spring 特定的东西包含到他们的代码库中,但我不明白为什么 Spring 人们不改进他们的测试工具。

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

  1. kafkaService.getClass()com.example.template.services.KafkaService$MockitoMock961867$$EnhancerBySpringCGLIB$fc4fe95
  2. kafkaService.getClass().getSuperclass()com.example.template.services.KafkaService$MockitoMock961867
  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 接口,该接口也提供对目标对象的访问),它只是 returns再次传递对象。

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

@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 代理。但是,自动连接的 bean kafkaService 仍然存在,因此 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();
}

P.S.: 没有你的 MCVE 我永远无法调试它并找出到底发生了什么。这再次证明,提出包括 MCVE 在内的问题是您可以为自己做的最好的事情,因为它可以帮助您获得问题的答案,否则这些问题可能仍然没有答案。


更新: 在我在类似的已关闭问题 Spring Boot #6871, one of the maintainers has by himself created Spring Boot #22281 下提到这个问题后,它专门解决了您的问题。您可能想观看新问题,以了解 if/when 它是否可以修复。