In this demo, there is a Mockito spy on a @Bean DemoService in a test class. DemoService has a single @Async method someAsyncTask. In a @Test method, someAsyncTask is mocked to throw a RuntimeException then called expecting this exception to be thrown. This test fails. When @EnableAsync is removed, this test passes.

Note: this issue was initially opened as a Spring Boot issue.

Comment From: redzi

I believe the issue is caused by DemoService being a proxy bean (because of @Async and how it's handled in Spring AOP). So mocking is done on a different object.

This test mocks the target object not the proxy and it passes:

@Test
void whenThenThrowTest() throws InterruptedException, ExecutionException {
    DemoService targetObject = (DemoService) AopTestUtils.getTargetObject(demoService);
    when(targetObject.someAsyncTask()).thenThrow(RuntimeException.class);
    Assertions.assertThrows(RuntimeException.class, () -> targetObject.someAsyncTask());
}

Both test cases in the demo project are also ok when @EnableAsync is set with AspectJ mode: @EnableAsync(mode = AdviceMode.ASPECTJ)

It works because there is no proxy involved, instead aspects are weaved directly into the code.

Comment From: snicoll

Correct and creating a spy behind the proxy like that is not going to work as you expected. Andy already provided the explanation in the Spring Boot issue:

The other part of the problem is that demoService.someAsyncTask() is being called on the proxy that's created to support @Async and not on the spy. This can be overcome by getting the proxy's target.