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
时,您会发现以下事实:
kafkaService.getClass()
是 com.example.template.services.KafkaService$MockitoMock961867$$EnhancerBySpringCGLIB$fc4fe95
kafkaService.getClass().getSuperclass()
是 com.example.template.services.KafkaService$MockitoMock961867
kafkaService.getClass().getSuperclass().getSuperclass()
是 class com.example.template.services.KafkaService
换句话说:
kafkaService
是 CGLIB Spring AOP 代理。
- AOP 代理包装了一个 Mockito 间谍(可能是一个 ByteBuddy 代理)。
- 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 它是否可以修复。
在集成测试中,我的 @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
时,您会发现以下事实:
kafkaService.getClass()
是com.example.template.services.KafkaService$MockitoMock961867$$EnhancerBySpringCGLIB$fc4fe95
kafkaService.getClass().getSuperclass()
是com.example.template.services.KafkaService$MockitoMock961867
kafkaService.getClass().getSuperclass().getSuperclass()
是class com.example.template.services.KafkaService
换句话说:
kafkaService
是 CGLIB Spring AOP 代理。- AOP 代理包装了一个 Mockito 间谍(可能是一个 ByteBuddy 代理)。
- 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 它是否可以修复。