5

I'm just starting to get familiar with the CompletableFuture tool from Java. I've created a little toy application to model some recurrent use case almost any dev would face.

In this example I simply want to save a thing in a DB, but before doing so I want to check if the thing was already saved.

If the thing is already in the DB the flow (the chain of completable futures) should stop and not save the thing. What I'm doing is throwing an exception so eventually I can handle it and give a good message to the client of the service so he can know what happened.

This is what I've tried so far:

First the code that try to save the thing or throw an error if the thing is already in the table:

repository
        .query(thing.getId())
        .thenCompose(
            mayBeThing -> {
              if (mayBeThing.isDefined()) throw new CompletionException(new ThingAlreadyExists());
              else return repository.insert(new ThingDTO(thing.getId(), thing.getName()));

And this is the test I'm trying to run:

    CompletableFuture<Integer> eventuallyMayBeThing =
        service.save(thing).thenCompose(i -> service.save(thing));
    try {
      eventuallyMayBeThing.get();
    } catch (CompletionException ce) {
      System.out.println("Completion exception " + ce.getMessage());
      try {
        throw ce.getCause();
      } catch (ThingAlreadyExist tae) {
        assert (true);
      } catch (Throwable t) {
        throw new AssertionError(t);
      }
    }

This way of doing it I took it from this response: Throwing exception from CompletableFuture ( the first part of the most voted answer ).

However, this is not working. The ThingAlreadyExist is being thrown indeed but it's never being handled by my try catch block. I mean, this:

catch (CompletionException ce) {
      System.out.println("Completion exception " + ce.getMessage());
      try {
        throw ce.getCause();
      } catch (ThingAlreadyExist tae) {
        assert (true);
      } catch (Throwable t) {
        throw new AssertionError(t);
      }

is never executed.

I have 2 questions,

  1. Is there a better way?

  2. If not, am I missing something? Why can't I handle the exception in my test?

Thanks!

Update(06-06-2019)

Thanks VGR you are right. This is the code working:

try {
      eventuallyMayBeThing.get();
    } catch (ExecutionException ce) {
      assertThat(ce.getCause(), instanceOf(ThingAlreadyExists.class));
    }
Yuliban
  • 143
  • 7
  • 1
    [get() does not throw a CompletionException](https://docs.oracle.com/en/java/javase/12/docs/api/java.base/java/util/concurrent/CompletableFuture.html#get%28%29). It throws an ExecutionException, which wraps whatever exception your task threw. Since it will be wrapping your explicitly created CompletionException, you will need to call getCause() twice to get to your ThingAlreadyExist. – VGR Jun 06 '19 at 00:01
  • 1
    There is no need to throw and immediately catch an exception. Just check `ce.getCause()` with `instanceof`s. – Andy Turner Jun 06 '19 at 00:06
  • A statement like `assert (true);` makes no sense. – Holger Jun 06 '19 at 17:28

2 Answers2

4

By unit testing your code wrapped up in a Future, you’re testing java’s Future framework. You shouldn’t test libraries - you either trust them or you don’t.

Instead, test that your code, in isolation, throws the right exceptions when it should. Break out the logic and test that.

You can also integration test your app to assert that your entire app behaves correctly (regardless of implementation).

Bohemian
  • 412,405
  • 93
  • 575
  • 722
  • I don't get what you mean. I'm not testing the library, I want to be able to get the expected exception so I can handle it and give the user the correct message. In this case I want to show him that the thing he's trying to save was already saved. – Yuliban Jun 06 '19 at 16:15
  • Getting the correct exception has nothing whatsoever to do with CompletableFuture. You must completely trust that if your code throws an exception, then it will be propogated by CompletableFuture if you use it. Given that, you need to create code, with a complete absence of the use of CompletableFuture, that throws the right exception in the right circumstance, and write tests for that code. Once that's working, write system tests that test if your app as a whole has the right behaviour. System tests should not have any reference to any aspect of implementation, such as CompletableFuture. – Bohemian Jun 06 '19 at 19:58
2

You have to be aware of the differences between get() and join().

The method get() is inherited from the Future interface and will wrap exceptions in an ExecutionException.

The method join() is specific to CompletableFuture and will wrap exceptions in a CompletionException, which is an unchecked exception, which makes it more suitable for the functional interfaces which do not declare checked exceptions.

That being said, the linked answer addresses use cases where the function has to do either, return a value or throw an unchecked exception, whereas your use case involves compose, where the function will return a new CompletionStage. This allows an alternative solution like

.thenCompose(mayBeThing -> mayBeThing.isDefined()?
    CompletableFuture.failedFuture​(new ThingAlreadyExists()):
    repository.insert(new ThingDTO(thing.getId(), thing.getName())))

CompletableFuture.failedFuture has been added in Java 9. If you still need Java 8 support, you may add it to your code base

public static <T> CompletableFuture<T> failedFuture(Throwable t) {
    final CompletableFuture<T> cf = new CompletableFuture<>();
    cf.completeExceptionally(t);
    return cf;
}

which allows an easy migration to a newer Java version in the future.

Holger
  • 285,553
  • 42
  • 434
  • 765