Reputation: 86757
I want to assert an exception that should be thrown within an @Async
void method.
The following fails, even though I already add a SyncTaskExecutor
explicit.
org.opentest4j.AssertionFailedError: Expected RuntimeException to be thrown, but nothing was thrown.
@TestConfiguration
public class SyncTaskExecutorTestConfiguration {
@Bean
@Primary
public TaskExecutor asyncExecutor() {
return new SyncTaskExecutor();
}
}
@SpringBootTest
@Import(SyncTaskExecutorTestConfiguration.class)
public class MyTest {
@Test
public void test() {
assertThrows(RuntimeException.class, () -> service.run());
}
}
@Service
@Async //also @EnableAsync existing on @Configuration class
public class AsyncService {
public void run() {
//of course real world is more complex with multiple sub calls here
throw new RuntimeException("junit test");
}
}
Upvotes: 2
Views: 1611
Reputation: 71
I'm facing the same problem.
bilak's post gave the idea of having my custom AsyncUncaughtExceptionHandler
declared with a @Component
annotation.
Then, in my custom implmentation of AsyncConfigurer
I was injecting my custom AsyncUncaughtExceptionHandler
.
In my tests, I used the @MockBean
annotation on my custom AsyncUncaughtExceptionHandler
, so I was able to verify that the handleUncaughtException
was called with the appropriate exception.
Code sample:
AsyncExceptionHandler
@Slf4j
@Component
public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable throwable, Method method, Object... objects) {
log.error("Exception while executing with message: {} ", throwable.getMessage());
log.error("Exception happen in {} method ", method.getName());
}
}
CustomAsyncConfigurer
@Configuration
public class CustomAsyncConfigurer implements AsyncConfigurer {
final private AsyncExceptionHandler asyncExceptionHandler;
@Autowired
public TaskExecutorConfiguration(AsyncExceptionHandler asyncExceptionHandler) {
this.asyncExceptionHandler = asyncExceptionHandler;
}
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("AsyncThread::");
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return asyncExceptionHandler;
}
}
My unit test:
class FooServiceTest extends FooApplicationTests {
@MockBean
private AsyncExceptionHandler asyncExceptionHandler;
@Autowired
private FooService fooService;
@Test
void testCreateEnrollmentBioStoreException() throws Exception {
fooService.doBar();
ArgumentCaptor<FooException> argumentCaptor = ArgumentCaptor.forClass(FooException.class);
verify(asyncExceptionHandler, times(1)).handleUncaughtException(argumentCaptor.capture(), any(), any());
FooException exception = argumentCaptor.getValue();
assertEquals("Foo error message", exception.getMessage());
}
}
I'm not sure if this is the right way, but I have a void method that was turned into async, so I didn't want to change the return value just for the tests.
Upvotes: 4
Reputation: 4932
What about using AsyncUncaughtExceptionHandler that will be defined for your AsyncConfigurer?
So basically when you execute your method which throws exception you can verify that exception was handled inside handler? Just an idea, didn't tried this.
Upvotes: 1
Reputation: 40068
Since the @Async
method get executed asynchronously by a thread from asyncExecutor
and it is terminated due to RuntimeException
which doesn't have any impact on Main thread, the actually Main-Test
thread competes successfully with the rest of flow once after it trigger the async call. So i will recommend to use the CompletableFuture to hold the reference of Async process always even it's required or not and truthfully will help in test cases
@Service
@Async
public class AsyncService {
public CompletableFuture<Void> run() {
//of course real world is more complex with multiple sub calls here
throw new RuntimeException("junit test");
}
}
So in the test you can wait for Async
thread to complete assert the cause from ExecutionException, Since the get
method throws ExecutionException
if this future completed exceptionally
CompletableFuture.allOf(wait);
One more note you can refer link for asserting wrapped exceptions
Upvotes: 2