-3

I want to test a method which asynchronously calls an API. Somewhat like below -

private void callExternalAPI(){

   CompletableFuture.runAsync(() -> {
      try{
         //Calling the API
      }catch(final Exception e){
         //logging some metrics
      }
   });

}

I am mocking the API call in the tests, returning some values of throwing some exceptions to test both the success case and exception case and verifying metrics. Using JUnit & Mockito to write tests.

The test completes its execution before the asynchronous part could complete logging the metric and the test could catch them, which gives me a Argument(s) are different, Wanted: Error

When I remove the CompletableFuture.runAsync part and make the code Synchronous, then the tests pass.

I tried multiple things like -

  1. Thread.sleep(5000) just after the test subject was called.
  2. In the verify(), I used timeout(5000) like below - verify(mockMetricLogger, timeout(5000).times(1)).....

I did not want to use Awaitility or Executors, they would unnecessarily make the code big.

Also, don't want to use supplyAsync or return anything from this method.

4
  • 1
    I mean you have your answer there. Use Awaitility or verification with timeout. "unnecessarily make the code big" doesnt make sense. Though you also mentioned using verification with timeout. Whats wrong with that? you just briefly mentioned using it and just glossed over it completely Commented Jun 9 at 14:14
  • Mockito doesn't support multithreading. You have to mock runAsync method. Commented Jun 9 at 15:09
  • 1
    I'll toss out an opinion: when you're testing a external API like this, you're doing integration testing, not unit testing. Break this method up, separate concerns so that the multithreading is not part of the rest of the code, and test the components in unit testing that can be tested in unit test. Don't try to test absolutely everything in unit testing, it won't work. Commented Jun 9 at 16:01
  • 2
    So your unit test is already a success: it revealed that caller of callExternalAPI() can not be sure that the operation has been performed nor know whether it was successful or not. When you’re fine with that, there’s no point in testing that method which is just a wrapper around the actual operation. If you think the actual operation is worth testing, it’s also worth being in a named method rather than an anonymous lambda expression. Move it into a method, change callExternalAPI() to CompletableFuture.runAsync(this::theNewMethod) and create a unit test for theNewMethod() instead. Commented Jun 10 at 12:32

1 Answer 1

2

Your implementation is on the right track with CompletableFuture.runAsync. Still, since it runs asynchronously, your test needs to wait until the background task completes, without changing the code or making the test overly complex.

You don’t need to use Awaitility or Executors, but you do need a deterministic and lightweight way to wait for async completion inside the test.

    @Test
    void testCallExternalAPILogsMetricsOnException() {
        // Given
        CountDownLatch latch = new CountDownLatch(1);

        // Mock API call to throw exception
        doAnswer(invocation -> {
            throw new RuntimeException("testCallExternalAPILogsMetricsOnException() => API execution failure");
        }).when(mockApiClient).call(); // Example mock

        // Mock metric logger to count down the latch
        doAnswer(invocation -> {
            latch.countDown(); // This will unblock the test
            return null;
        }).when(mockMetricLogger).log(any());

        // When
        myService.callExternalAPI();

        // Then
        boolean isCompleted = latch.await(5, TimeUnit.SECONDS); // Wait max 5s
        assertTrue(isCompleted, "testCallExternalAPILogsMetricsOnException() => Async logging didn't finish in time");
        verify(mockMetricLogger, times(1)).log(any());
    }

Try this method, this should help you get yours query resolved.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.