Einführung in die Wartbarkeit

1. Einleitung

Ein häufiges Problem bei asynchronen Systemen besteht darin, dass es schwierig ist, lesbare Tests für sie zu schreiben, die sich auf die Geschäftslogik konzentrieren und nicht durch Synchronisierungen, Zeitüberschreitungen und Parallelitätskontrolle verschmutzt sind.

In diesem Artikel werfen wir einen Blick auf Awaitility - eine Bibliothek, die eine einfache domänenspezifische Sprache (DSL) für asynchrone Systemtests bereitstellt .

Mit Awaitility können wir unsere Erwartungen an das System in einem einfach zu lesenden DSL ausdrücken.

2. Abhängigkeiten

Wir müssen unserer pom.xml Awaitility-Abhängigkeiten hinzufügen .

Die Wartebibliothek ist für die meisten Anwendungsfälle ausreichend. Wenn wir Proxy-basierte Bedingungen verwenden möchten , müssen wir auch die Wait-Proxy- Bibliothek bereitstellen :

 org.awaitility awaitility 3.0.0 test   org.awaitility awaitility-proxy 3.0.0 test 

Sie finden die neueste Version der Bibliotheken " Warten" und " Warten auf Proxy" in Maven Central.

3. Erstellen eines asynchronen Dienstes

Schreiben wir einen einfachen asynchronen Dienst und testen ihn:

public class AsyncService { private final int DELAY = 1000; private final int INIT_DELAY = 2000; private AtomicLong value = new AtomicLong(0); private Executor executor = Executors.newFixedThreadPool(4); private volatile boolean initialized = false; void initialize() { executor.execute(() -> { sleep(INIT_DELAY); initialized = true; }); } boolean isInitialized() { return initialized; } void addValue(long val) { throwIfNotInitialized(); executor.execute(() -> { sleep(DELAY); value.addAndGet(val); }); } public long getValue() { throwIfNotInitialized(); return value.longValue(); } private void sleep(int delay) { try { Thread.sleep(delay); } catch (InterruptedException e) { } } private void throwIfNotInitialized() { if (!initialized) { throw new IllegalStateException("Service is not initialized"); } } }

4. Testen mit Wartbarkeit

Jetzt erstellen wir die Testklasse:

public class AsyncServiceLongRunningManualTest { private AsyncService asyncService; @Before public void setUp() { asyncService = new AsyncService(); } //... }

Unser Test prüft, ob die Initialisierung unseres Dienstes innerhalb eines bestimmten Zeitlimits (Standard 10s) nach dem Aufruf der Initialisierungsmethode erfolgt .

Dieser Testfall wartet lediglich darauf, dass sich der Dienstinitialisierungsstatus ändert, oder löst eine ConditionTimeoutException aus, wenn die Statusänderung nicht auftritt.

Der Status wird von einem Callable abgerufen , der unseren Service in definierten Intervallen (Standard 100 ms) nach einer bestimmten anfänglichen Verzögerung (Standard 100 ms) abfragt. Hier verwenden wir die Standardeinstellungen für Timeout, Intervall und Verzögerung:

asyncService.initialize(); await() .until(asyncService::isInitialized);

Hier verwenden wir await - eine der statischen Methoden der Awaitility- Klasse. Es gibt eine Instanz einer ConditionFactory- Klasse zurück. Wir können auch andere Methoden verwenden , um die Lesbarkeit zu verbessern.

Die Standard-Timing-Parameter können mit statischen Methoden aus der Awaitility- Klasse geändert werden :

Awaitility.setDefaultPollInterval(10, TimeUnit.MILLISECONDS); Awaitility.setDefaultPollDelay(Duration.ZERO); Awaitility.setDefaultTimeout(Duration.ONE_MINUTE);

Hier sehen wir die Verwendung der Duration- Klasse, die nützliche Konstanten für die am häufigsten verwendeten Zeiträume bereitstellt.

Wir können auch benutzerdefinierte Timing-Werte für jeden wartenden Anruf bereitstellen . Hier erwarten wir, dass die Initialisierung spätestens nach fünf Sekunden und mindestens nach 100 ms mit Abfrageintervallen von 100 ms erfolgt:

asyncService.initialize(); await() .atLeast(Duration.ONE_HUNDRED_MILLISECONDS) .atMost(Duration.FIVE_SECONDS) .with() .pollInterval(Duration.ONE_HUNDRED_MILLISECONDS) .until(asyncService::isInitialized);

Es ist erwähnenswert , dass der ConditionFactory enthält weitere Methoden wie mit , dann , und , gegeben. Diese Methoden tun nichts und geben dies nur zurück , aber sie könnten nützlich sein, um die Lesbarkeit der Testbedingungen zu verbessern.

5. Verwenden von Matchern

Die Wartbarkeit ermöglicht auch die Verwendung von Hamcrest-Matchern , um das Ergebnis eines Ausdrucks zu überprüfen. Zum Beispiel können wir überprüfen, ob unser langer Wert nach dem Aufruf der addValue- Methode wie erwartet geändert wird:

asyncService.initialize(); await() .until(asyncService::isInitialized); long value = 5; asyncService.addValue(value); await() .until(asyncService::getValue, equalTo(value));

Beachten Sie, dass wir in diesem Beispiel den ersten Warteanruf verwendet haben, um zu warten, bis der Dienst initialisiert wird. Andernfalls würde die Methode getValue eine IllegalStateException auslösen .

6. Ausnahmen ignorieren

Manchmal kommt es vor, dass eine Methode eine Ausnahme auslöst, bevor ein asynchroner Job ausgeführt wird. In unserem Service kann es sich um einen Aufruf der getValue- Methode handeln, bevor der Service initialisiert wird.

Die Wartbarkeit bietet die Möglichkeit, diese Ausnahme zu ignorieren, ohne einen Test zu bestehen.

Lassen Sie uns beispielsweise überprüfen, ob das Ergebnis getValue direkt nach der Initialisierung gleich Null ist, und die IllegalStateException ignorieren :

asyncService.initialize(); given().ignoreException(IllegalStateException.class) .await().atMost(Duration.FIVE_SECONDS) .atLeast(Duration.FIVE_HUNDRED_MILLISECONDS) .until(asyncService::getValue, equalTo(0L));

7. Verwenden von Proxy

Wie in Abschnitt 2 beschrieben, müssen wir den Wait-Proxy einschließen , um Proxy-basierte Bedingungen zu verwenden. Die Idee des Proxys besteht darin, echte Methodenaufrufe für Bedingungen ohne Implementierung eines Callable- oder Lambda-Ausdrucks bereitzustellen .

Verwenden Sie die statische Methode AwaitilityClassProxy.to , um zu überprüfen, ob AsyncService initialisiert ist:

asyncService.initialize(); await() .untilCall(to(asyncService).isInitialized(), equalTo(true));

8. Zugriff auf Felder

Awaitility kann sogar auf private Felder zugreifen, um Zusicherungen für diese Felder durchzuführen. Im folgenden Beispiel sehen wir eine andere Möglichkeit, den Initialisierungsstatus unseres Dienstes abzurufen:

asyncService.initialize(); await() .until(fieldIn(asyncService) .ofType(boolean.class) .andWithName("initialized"), equalTo(true));

9. Fazit

In diesem kurzen Tutorial haben wir die Awaitility-Bibliothek vorgestellt, uns mit ihrem grundlegenden DSL zum Testen asynchroner Systeme vertraut gemacht und einige erweiterte Funktionen kennengelernt, die die Bibliothek flexibel und einfach in realen Projekten verwenden.

Wie immer sind alle Codebeispiele auf Github verfügbar.