Assertions mit Optionals

Optionals sind ein wahrer Segen in den Java API. Sie reduzieren eine Menge Boiler-Plate Code bei der Behandlung von fehlenden Rückgabewerten auf ein Minimum und verbessern die Semantik ungemein.

In Legacy Code findet sich noch häufig folgende Behandlung.

Output output = service.getOutput(input);
if (output == null) {
  throw new ServiceException();
}

Sehr viel einfacher und lesbarer ist die Verwendung eines Optionals als Ruckgabewert.

Output output = service.getOutput(input).orElseThrow(() -> new ServiceException());

Neben der kompakteren Form hat auch die Semantik der Methode gewonnen. Der Rückgabewert Optional<Output> signalisiert dem Benutzer, dass nicht immer mit einem definierten Rückgabewert zu rechnen ist.

In Unit Test wird der Rückgabewert häufig mit get ausgelesen und geprüft.

Optional<Output> result = service.getOutput(input);
assertTrue(result.get().hasFlag();

Leider beschwert sich die ein oder andere IDE, über den saloppen Zugriff über get ohne eine vorherige Prüfung auf einen existierenden Inhalt. Das darf man der IDE nicht verübeln, denn prüft man den Wert nicht in irgendeiner Form vorher, dann muss man eigentlich nachher den Wert auf null prüfen. Damit wäre der Einsatz eines Optionals aber ad absurdum geführt.

Der nachfolgende Ansatz wird dann häufig in den Unit Tests verwendet,

Optional<Output> result = service.getOutput(input);
assertTrue(result.isPresent());
assertTrue(result.get().hasFlag();

Dann ist zwar die IDE glücklich, aber nicht die Kollegen, denn die zusätzliche Zeile ist unelegant.

Zwei weiter Möglichkeiten nutzen die Vorzüge der Optional API besser.

Die obere Zeile liefert im Falle, dass die Variable nicht gesetzt ist, einen unpassenden Wert zurück und die untere wirft eine Exception.

Optional<Output> result = service.getOutput(input);
assertTrue(result.map(Output::hasFlag).orElse(false));
assertTrue(result.map(Output::hasFlag).orElseThrow(new RuntimeException());

Beide Varianten sind nicht wirklich schön, weil bei der ersten Variante im Fehlerfall nicht klar ist, ob output einen leeren Wert enthält oder hasFlag den Wert false zurückliefert. Im zweiten Fall missfällt das absichliche Werfen einer Exception im Test.

Mit dem Werfen einer Exception bewegt man sich aber schon auf eine gute Lösung zu. Im Framework werfen nämlich alle fehlschlagenden Assertions eine spezielle Exception. Und es existiert mit fail eine passende Assertion, die immer fehlschlägt.

Output result = service.getOutput(input).orElseGet(Assertions::fail);
assertTrue(result.hasFlag());