0

I have a method:

public sendMessageAsync(final PublishRequest request) {

    final CompletableFuture<PublishResponse> responseCompletableFuture = m_serviceAClient.publish(request);

    responseCompletableFuture.whenCompleteAsync((response, exception) -> {
       if (exception != null) {
            try {
                m_serviceBClient.sendMessage(request.getMessage())
            } catch (Exception e) {
               log.error("Failed to send message to fallback service.");
            }
         } else {
            log.info("Successfully called service client");   
         }
      }
    return;
}

Unit test:

public void testsendMessageAsyncWhenServiceAThrowsException() {
        CompletableFuture<PublishResponse> failedFuture = CompletableFuture.failedFuture(
                new RuntimeException("Failed to publish message to Service A"));
        doReturn(failedFuture).when(mockServiceAClient).publish(any(PublishRequest.class));
        doReturn("message-id").when(mockServiceBClient).sendMessage(anyString());
        MyClass.sendMessageAsync(new PublishRequest("test-message"));
}

I do not see the jacoco unit test coverage covering the line m_serviceBClient.sendMessage(request.getMessage()).

I believe this is because by the time future callback handler executes, the junit test is already finished, so the code coverage tool doesn't detect the future callback statement coverage.

alchems
  • 33
  • 3

1 Answers1

0

What I do usually to make this kind of methods testable is using a CountDownLatch.

For example, you may modify your class by creating another method (that you can make private but visible only for testing through the appropriate annotation so that the users of your API do not see it) that takes in input a CountDownLatch:

public void sendMessageAsync(final PublishRequest request, CountDownLatch latch) {

    final CompletableFuture<PublishResponse> responseCompletableFuture = m_serviceAClient.publish(request);

    responseCompletableFuture.whenCompleteAsync((response, exception) -> {
       if (exception != null) {
            try {
                m_serviceBClient.sendMessage(request.getMessage())
            } catch (Exception e) {
               log.error("Failed to send message to fallback service.");
            }
         } else {
            log.info("Successfully called service client");   
         }
      }
    latch.countDown();
    return;
}

Then in your test, you create a CountDownLatch with count 1 and await for it before the test ends:

CountDownLatch testLatch = new CountDownLatch(1);
MyClass.sendMessageAsync(new PublishRequest("test-message"), testLatch);
testLatch.await(1, TimeUnit.MINUTES);

Like that, you make your method testable even if it is asynchronous. Of course, you will create a method with the original signature (without latch) that simply calls the new method by passing a sample latch that will be ignored by production code.

To give some further explanation, the CountDownLatch is a concurrent utility that allows you to countDown in a multi-threading context (meaning one thread can countdown and the other threads will see the same value) and also allows you to .await (with a timeout if wanted) that the CountDown reaches zero before the thread which await is released. It is a pretty practical way to let another thread doing some work while the main thread waits for that work to be done (in your case, your test thread waits for the completable future thread to do what it has to do before to complete the test)

Matteo NNZ
  • 11,930
  • 12
  • 52
  • 89
  • I'd put that latch into the mock implementation of `m_serviceBClient.sendMessage` instead. That way the test functionality does not leak into production code. – michid Aug 22 '22 at 05:49
  • 1
    @michid but you would be counting down only in case of exception != null, I guess the code shown is just a sample and not the real code – Matteo NNZ Aug 22 '22 at 06:35