How to assert exceptions in @Async void methods?

1.4k views Asked by At

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");
    }
}
3

There are 3 answers

2
Ryuzaki L On

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

1
bilak On

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.

0
alobar On

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.