Automatisiertes JUnit-Testing von asynchronen Methoden

Falls Du in Deinen Unit-Test Klassen "Thread.sleep" verwendest, um z.B. die Ergebnisse asynchroner Methoden zu testen, dann zeige ich Dir im folgenden Artikel die clean code Variante.

Was ist das Problem mit Thread.sleep in Unit-Tests?

Unser Team entwickelt zur Zeit einen Import Service, der große Datenmengen einliest und verarbeitet. Der Import Prozess dauert relativ lange, daher ist eine asynchrone Verarbeitung des Imports eine nahe liegende Lösung, die vereinfacht so aussehen könnte:

public void importDataAsync() throws Exception {
  // Java 8 lambda expression for a thread.
  Runnable asyncImport = () -> {
    // Import data takes a long time.
    // Writes state 'done' in database, 
    // when import finished successful.
  };
  // Java 8 solution to run threads asynchronously.
  CompletableFuture.runAsync(asyncImport)n
      .orTimeout(120, TimeUnit.SECONDS);
}

Wie könnte nun ein einfacher JUnit Test dazu aussehen? An anderen Stellen fand ich diese Thread.sleep-Lösung:

private DataImporter importer;

@Test
public void importDataAsyncTest() {
  importer.importDataAsync();
  // Sleep one minute.
  Thread.sleep(60000l);
  assertTrue(/* database contains state 'done'*/);
}

Dieser Test hat 2 entscheidende Nachteile:
  • Wenn die Schlafenszeit von Thread.sleep zu kurz gewählt ist, schlägt der Test fehl, weil der Zustand 'done' noch nicht durch die asynchrone Verarbeitung in die Datenbank geschrieben wurde.
    Da Threads manchmal schneller und manchmal langsamer laufen, kann es auch passieren, dass unser Test manchmal erfolgreich ist und manchmal fehlschlägt.
    Instabilität in JUnit-Tests ist wesentlich unangenehmer als ein Test der direkt fehlschlägt und dann auch direkt vom Entwickler behandelt werden kann.
  • Wenn die Schlafenszeit zu lang gewählt ist, schläft der Entwickler ein, während er auf die Ergebnisse seiner JUnit Tests wartet 😉
    Langsame Tests sind ein echtes Problem! Wenn die Schlafenszeit auf 1 Minute gesetzt ist, das Entwickler-Team aus 5 Personen besteht und jeder 2 Mal am Tag alle Tests ausführt, dann verschwendet dieser eine Test im Jahr xyz Stunden Arbeitszeit - viel Spaß beim Rechnen 😜 
     
Man könnte nun Thread-sleep in eine Schleife packen und immer wieder prüfen, ob Zustand 'done' geschrieben wurde. Das wäre aber unnötiger Code, da andere das schon für uns getan haben.

Was macht Awaitility besser?

SonarQube empfiehlt die Verwendung von mocks oder Bibliotheken wie Awaitility anstelle von Thread.sleep.
Mit Awaitility kann man Erwartungen an den Zustand von asynchronen Systemen definieren. Diese Erwartungen lassen sich knapp formulieren und sind leicht lesbar - beides typische Clean Code Anforderungen.
Konkret könnte das zum Beispiel so aussehen:

// Warte bis die Methode isStatusDone() true zurück gibt 
// oder werfe nach 5 Sekunden eine TimeoutException.
await().atMost(5, TimeUnit.SECONDS).until(isStatusDone()); 

// Warte bis ein Mock mindestens einmal benutzt wurde 
// (implementiert in isMockUsedAtLeastOnce),
// ignoriere aber alle AssertionError, 
// die der Mock wirft bevor er das erste mal benutzt wurde.
await().ignoreException(AssertionError.class)
    .until(isMockUsedAtLeastOnce());

Awaitility bietet noch viel mehr Möglichkeiten diese Bedingungen auszudrücken, dazu verweise ich aber auf die offizielle Dokumentation von Awaitility:

JUnit Test einer asynchronen Methode

Zum Abschluss noch ein vollständiges Beispiel zur Nutzung von Awaitility in JUnit Tests.
Die Awaitility Bibliothek muss mit einen Build-Tool wie Maven geladen werden:

<dependency>
    <groupId>org.awaitility</groupId>
    <artifactId>awaitility</artifactId>
    <version>4.0.3</version>
    <scope>test</scope>
</dependency>

Im JUnit Test kann Awaitility mittels statischem Import definiert werden. 
Danach kann man prüfen, ob die asynchrone Methode den Zustand 'done' korrekt in der Datenbank gesetzt hat:

import static org.awaitility.Awaitility.*;
import static org.junit.Assert.assertTrue;
...
private DataImporter importer;

@Test
public void importDataAsyncTest() {
  importer.importDataAsync();
  // Schlafe 10 Sekunden, 
  // danach warte bis die Methode isStatusDone() true zurück gibt
  // oder werfe nach 1 Minute eine TimeoutException. 
  await().pollDelay(10, TimeUnit.SECONDS)
      .atMost(1, TimeUnit.MINUTES).until(isStatusDone()); 
  assertTrue(/* database contains state 'done'*/);
}

Mit pollDelay kann Awaitility sogar schlafen - sinnvoll wenn man sicher weiß, dass die asynchrone Methode am Anfang eine gewisse Zeit braucht, bevor sie testbare Ergebnisse produziert hat. Wie man mit Awaitility nun ein Thread.sleep nachbaut nur damit SonarQube keinen Code Smell mehr anzeigt, möchte ich euch in meinem Clean Coding Blog aber nicht zeigen 😈

Kommentare

Beliebte Posts aus diesem Blog

OpenID Connect mit Spring Boot 3

Authentifizierung in Web-Anwendungen mit Spring Security 6

Validierung per Annotation in Java und Spring