Nanor
Nanor

Reputation: 2550

CompletableFuture.allOf().get() does not run in a unit test

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?

Upvotes: 1

Views: 1781

Answers (1)

i.bondarenko
i.bondarenko

Reputation: 3572

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.

Upvotes: 4

Related Questions