Issue
I have a method which asynchronously builds several DTOs. It works well in general use so I'm trying to write some unit tests for it. The method looks like:
public List<SurgeClientDto> clientLeaderboard(@RequestBody List<String> accountIds) throws ExecutionException, InterruptedException {
List<SurgeClientDto> surgeClients = new ArrayList<>(accountIds.size());
long start = System.currentTimeMillis();
List<CompletableFuture> futures = new ArrayList<>();
for (String accountId : accountIds) {
futures.add(
CompletableFuture.runAsync(() -> {
buildSurgeClientDto(surgeClients, accountId);
}, executor)
);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()])).get();
log.info("Time taken: {}ms", System.currentTimeMillis() - start);
return surgeClients;
}
and my test looks like:
@Test
@DirtiesContext
public void testGetLeaderboard() throws Exception {
// Given
final List<String> accounts = new ArrayList<>();
final String accountOne = "ABCDE";
final String accountTwo = "ZYXWV";
final String accountThree = "FAKE!";
final String clientForename = "John";
final String clientSurname = "Smith";
ClientDetailsCursorResult validOne = ClientDetailsCursorResult.builder()
.accountId(accountOne)
.forename(clientForename)
.surname(clientSurname)
.build();
ClientDetailsCursorResult validTwo = ClientDetailsCursorResult.builder()
.accountId(accountTwo)
.forename(clientForename)
.surname(clientSurname)
.build();
BalanceDetailsDto validBalanceDetailsDto = new BalanceDetailsDto();
validBalanceDetailsDto.setAvailableToWithdraw(100d);
validBalanceDetailsDto.setAvailableBalance(100d);
accounts.add(accountOne);
accounts.add(accountTwo);
accounts.add(accountThree);
// When
when(accountMaintenanceRestClient.getAccount(accountOne)).thenReturn(accountDTO());
when(accountMaintenanceRestClient.getAccount(accountTwo)).thenReturn(accountDTO());
when(accountMaintenanceRestClient.getAccount(accountThree)).thenReturn(null);
when(clientDetailsJdbc.getClientAccounts(accountOne)).thenReturn(Arrays.asList(validOne));
when(clientDetailsJdbc.getClientAccounts(accountTwo)).thenReturn(Arrays.asList(validTwo));
when(balanceDetailsService.getBalanceDetails(accountOne)).thenReturn(validBalanceDetailsDto);
when(balanceDetailsService.getBalanceDetails(accountTwo)).thenReturn(validBalanceDetailsDto);
List<SurgeClientDto> surgeClientDtos = surgeParisController.clientLeaderboard(accounts);
// Then
assertThat(surgeClientDtos.get(0).getAccountId(), is(accountOne));
assertThat(surgeClientDtos.get(0).getAvailableToTrade(), is(100d));
assertThat(surgeClientDtos.get(0).getAvailableToWithdraw(), is(100d));
assertThat(surgeClientDtos.get(0).getClientName(), is(clientForename + " " + clientSurname));
}
When I run my test it gets stuck in an infinite loop with no output. When I debug the code the last line to execute is
CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()])).get();
I have placed a breakpoint within buildSurgeClientDto()
and tried running in debug mode again but the breakpoint is never triggered.
Why is this? Is there something special I have to do to test async CompletableFutures
?
Solution
The issue is that you mock Executor
. It just never execute the task, so the test is just hang. What you can do is just use simple executor and inject it into your controller:
private Executor executor = Executors.newSingleThreadExecutor();
instead of
@Mock
private Executor executor;
For your test it should work. You do not need to test Executor
and CompletableFuture
because it is part of JDK and well tested alredy.
But if you need mock executor you should mock or stub:
// CompletableFuture code:
executor.execute(new AsyncRun(dep, function));
otherwise the test will hang.
Answered By - i.bondarenko