Wie testen Sie Methoden, die asynchrone Prozesse mit JUnit auslösen?
Ich weiß nicht, wie ich meinen Test auf das Ende des Prozesses warten lassen soll (es ist nicht gerade ein Komponententest, es ist eher ein Integrationstest, da es mehrere Klassen und nicht nur eine umfasst).
Antworten:
IMHO ist es eine schlechte Praxis, Unit-Tests erstellen zu lassen oder auf Threads usw. zu warten. Sie möchten, dass diese Tests in Sekundenbruchteilen ausgeführt werden. Aus diesem Grund möchte ich einen zweistufigen Ansatz zum Testen von asynchronen Prozessen vorschlagen.
quelle
Eine Alternative ist die Verwendung der CountDownLatch- Klasse.
HINWEIS Sie können nicht einfach mit einem regulären Objekt synchronisiert als Sperre verwenden, da schnelle Rückrufe die Sperre aufheben können, bevor die Wartemethode der Sperre aufgerufen wird. Siehe diesen Blog-Beitrag von Joe Walnes.
BEARBEITEN Synchronisierte Blöcke um CountDownLatch wurden dank Kommentaren von @jtahlborn und @Ring entfernt
quelle
Sie können versuchen, die Awaitility- Bibliothek zu verwenden. Es macht es einfach, die Systeme zu testen, über die Sie sprechen.
quelle
CountDownLatch
(siehe Antwort von @Martin) in dieser Hinsicht besser ist.Wenn Sie eine CompletableFuture (in Java 8 eingeführt) oder eine SettableFuture (von Google Guava ) verwenden, können Sie Ihren Test sofort beenden, anstatt eine voreingestellte Zeit zu warten. Ihr Test würde ungefähr so aussehen:
quelle
Starten Sie den Prozess und warten Sie mit a auf das Ergebnis
Future
.quelle
Eine Methode, die ich zum Testen asynchroner Methoden als sehr nützlich empfunden habe, ist das Einfügen einer
Executor
Instanz in den Konstruktor des zu testenden Objekts. In der Produktion ist die Executor-Instanz so konfiguriert, dass sie asynchron ausgeführt wird, während sie im Test verspottet werden kann, um synchron ausgeführt zu werden.Angenommen, ich versuche, die asynchrone Methode zu testen
Foo#doAsync(Callback c)
.In der Produktion würde ich
Foo
mit einerExecutors.newSingleThreadExecutor()
Executor-Instanz konstruieren, während ich sie im Test wahrscheinlich mit einem synchronen Executor konstruieren würde, der Folgendes ausführt:Jetzt ist mein JUnit-Test der asynchronen Methode ziemlich sauber -
quelle
WebClient
Es ist an sich nichts Falsches daran, Threaded / Async-Code zu testen, insbesondere wenn Threading der Punkt des Codes ist, den Sie testen. Der allgemeine Ansatz zum Testen dieses Materials ist:
Aber das ist eine Menge Boilerplate für einen Test. Ein besserer / einfacherer Ansatz ist die Verwendung von ConcurrentUnit :
Der Vorteil gegenüber dem
CountdownLatch
Ansatz besteht darin, dass er weniger ausführlich ist, da Assertionsfehler, die in einem Thread auftreten, ordnungsgemäß an den Hauptthread gemeldet werden, was bedeutet, dass der Test fehlschlägt, wenn er sollte. Eine Beschreibung, die denCountdownLatch
Ansatz von ConcurrentUnit vergleicht , finden Sie hier .Ich habe auch einen Blog-Beitrag zum Thema für diejenigen geschrieben, die etwas mehr Details erfahren möchten.
quelle
Wie wäre es mit einem Aufruf
SomeObject.wait
undnotifyAll
wie hier beschrieben ODER mit der Robotiums-Solo.waitForCondition(...)
Methode ODER mit einer Klasse, die ich dazu geschrieben habe ( Informationen zur Verwendung finden Sie in den Kommentaren und in der Testklasse )quelle
Ich finde eine Bibliothek socket.io zum Testen der asynchronen Logik. Mit LinkedBlockingQueue sieht es einfach und kurz aus . Hier ist ein Beispiel :
Wenn Sie LinkedBlockingQueue verwenden, blockieren Sie die API, bis Sie das Ergebnis wie auf synchrone Weise erhalten. Stellen Sie eine Zeitüberschreitung ein, um zu vermeiden, dass zu viel Zeit für das Warten auf das Ergebnis benötigt wird.
quelle
Es ist erwähnenswert, dass es
Testing Concurrent Programs
in Concurrency in Practice ein sehr nützliches Kapitel gibt, in dem einige Unit-Test-Ansätze beschrieben und Lösungen für Probleme angegeben werden.quelle
Dies ist, was ich heutzutage verwende, wenn das Testergebnis asynchron erzeugt wird.
Mit statischen Importen liest sich der Test ein bisschen nett. (Beachten Sie, dass ich in diesem Beispiel einen Thread beginne, um die Idee zu veranschaulichen.)
Wenn
f.complete
nicht aufgerufen, schlägt der Test nach einer Zeitüberschreitung fehl. Sie können auch verwenden,f.completeExceptionally
um früh zu scheitern.quelle
Hier gibt es viele Antworten, aber eine einfache besteht darin, einfach eine fertige CompletableFuture zu erstellen und diese zu verwenden:
Also in meinem Test:
Ich stelle nur sicher, dass all dieses Zeug trotzdem angerufen wird. Diese Technik funktioniert, wenn Sie diesen Code verwenden:
Es wird direkt durchlaufen, wenn alle CompletableFutures fertig sind!
quelle
Vermeiden Sie es, mit parallelen Threads zu testen, wann immer Sie können (was meistens der Fall ist). Dadurch werden Ihre Tests nur schuppig (manchmal bestanden, manchmal nicht bestanden).
Nur wenn Sie eine andere Bibliothek / ein anderes System aufrufen müssen, müssen Sie möglicherweise auf andere Threads warten. Verwenden Sie in diesem Fall immer die Awaitility- Bibliothek anstelle von
Thread.sleep()
.Rufen Sie niemals einfach an
get()
oder nehmen Siejoin()
an Ihren Tests teil, sonst werden Ihre Tests möglicherweise für immer auf Ihrem CI-Server ausgeführt, falls die Zukunft nie abgeschlossen wird. Setzen SieisDone()
Ihre Tests immer zuerst durch, bevor Sie anrufenget()
. Für CompletionStage also.toCompletableFuture().isDone()
.Wenn Sie eine nicht blockierende Methode wie diese testen:
dann sollten Sie nicht nur das Ergebnis testen , indem Sie ein ausgefülltes Zukunft in dem Test bestanden, sollten Sie auch sicherstellen , dass Ihre Methode
doSomething()
blockiert nicht durch den Aufrufjoin()
oderget()
. Dies ist insbesondere wichtig, wenn Sie ein nicht blockierendes Framework verwenden.Testen Sie dazu mit einer nicht abgeschlossenen Zukunft, die Sie manuell abgeschlossen haben:
Auf diese Weise
future.join()
schlägt der Test fehl , wenn Sie doSomething () hinzufügen .Wenn Ihr Dienst einen ExecutorService wie in verwendet
thenApplyAsync(..., executorService)
, fügen Sie in Ihren Tests einen ExecutorService mit einem Thread wie den von guava ein:Wenn Ihr Code den forkJoinPool verwendet, z. B.
thenApplyAsync(...)
, schreiben Sie den Code neu, um einen ExecutorService zu verwenden (es gibt viele gute Gründe), oder verwenden Sie Awaitility.Um das Beispiel zu verkürzen, habe ich BarService zu einem Methodenargument gemacht, das im Test als Java8-Lambda implementiert wurde. In der Regel handelt es sich um eine injizierte Referenz, die Sie verspotten würden.
quelle
Ich bevorzuge es zu warten und zu benachrichtigen. Es ist einfach und klar.
Grundsätzlich müssen wir eine endgültige Array-Referenz erstellen, die innerhalb einer anonymen inneren Klasse verwendet werden soll. Ich würde lieber einen Booleschen Wert [] erstellen, da ich einen Wert zur Steuerung setzen kann, wenn wir warten müssen (). Wenn alles erledigt ist, geben wir einfach asyncExecuted frei.
quelle
Für alle Spring-Benutzer da draußen mache ich heutzutage normalerweise meine Integrationstests, bei denen es um asynchrones Verhalten geht:
Lösen Sie ein Anwendungsereignis im Produktionscode aus, wenn eine asynchrone Aufgabe (z. B. ein E / A-Aufruf) abgeschlossen ist. Meistens ist dieses Ereignis ohnehin erforderlich, um die Reaktion des asynchronen Vorgangs in der Produktion zu verarbeiten.
Mit diesem Ereignis können Sie in Ihrem Testfall die folgende Strategie anwenden:
Um dies zu beheben, benötigen Sie zunächst eine Art Domain-Ereignis, um ausgelöst zu werden. Ich verwende hier eine UUID, um die abgeschlossene Aufgabe zu identifizieren, aber Sie können natürlich auch etwas anderes verwenden, solange es eindeutig ist.
(Beachten Sie, dass die folgenden Codefragmente auch Lombok- Anmerkungen verwenden, um den Kesselplattencode zu entfernen.)
Der Produktionscode selbst sieht dann normalerweise so aus:
Ich kann dann eine Feder verwenden
@EventListener
, um das veröffentlichte Ereignis im Testcode abzufangen. Der Ereignis-Listener ist etwas komplizierter, da er zwei Fälle threadsicher behandeln muss:A
CountDownLatch
wird für den zweiten Fall verwendet, wie in anderen Antworten hier erwähnt. Beachten Sie außerdem, dass die@Order
Anmerkung zur Ereignishandlermethode sicherstellt, dass diese Ereignishandlermethode nach allen anderen in der Produktion verwendeten Ereignislistenern aufgerufen wird.Der letzte Schritt besteht darin, das zu testende System in einem Testfall auszuführen. Ich verwende hier einen SpringBoot-Test mit JUnit 5, aber dies sollte für alle Tests mit einem Spring-Kontext gleich funktionieren.
Beachten Sie, dass diese Lösung im Gegensatz zu anderen Antworten hier auch funktioniert, wenn Sie Ihre Tests parallel ausführen und mehrere Threads gleichzeitig den asynchronen Code ausführen.
quelle
Wenn Sie die Logik testen möchten, testen Sie sie nicht asynchron.
Zum Beispiel, um diesen Code zu testen, der mit Ergebnissen einer asynchronen Methode arbeitet.
Im Test verspotten Sie die Abhängigkeit mit synchroner Implementierung. Der Unit-Test ist vollständig synchron und läuft in 150ms.
Sie testen das asynchrone Verhalten nicht, können aber testen, ob die Logik korrekt ist.
quelle