Wie testen Sie Methoden, die mit JUnit asynchrone Prozesse auslösen?
Ich weiß nicht, wie ich meinen Test warten lassen soll, bis der Prozess beendet ist (es ist nicht genau ein Komponententest, es ist eher ein Integrationstest, da er mehrere Klassen und nicht nur eine umfasst).
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.
Eine Alternative ist die Verwendung der Klasse CountDownLatch .
public class DatabaseTest {
/**
* Data limit
*/
private static final int DATA_LIMIT = 5;
/**
* Countdown latch
*/
private CountDownLatch lock = new CountDownLatch(1);
/**
* Received data
*/
private List<Data> receiveddata;
@Test
public void testDataRetrieval() throws Exception {
Database db = new MockDatabaseImpl();
db.getData(DATA_LIMIT, new DataCallback() {
@Override
public void onSuccess(List<Data> data) {
receiveddata = data;
lock.countDown();
}
});
lock.await(2000, TimeUnit.MILLISECONDS);
assertNotNull(receiveddata);
assertEquals(DATA_LIMIT, receiveddata.size());
}
}
[~ # ~] note [~ # ~] Sie können nicht einfach syncronized mit einem regulären Objekt als Sperre, da schnelle Rückrufe die Sperre aufheben können, bevor die Wartemethode der Sperre aufgerufen wird. Siehe this Blogeintrag von Joe Walnes.
[~ # ~] edit [~ # ~] Synchronisierte Blöcke um CountDownLatch wurden dank Kommentaren von @jtahlborn und @Ring entfernt
Sie können versuchen, die Bibliothek Awaitility zu verwenden. Es macht es einfach, die Systeme zu testen, über die Sie sprechen.
Wenn Sie ein CompletableFuture (eingeführt in Java 8) oder ein SettableFuture (von Google Guava ) verwenden, Sie können Ihren Test beenden, sobald er abgeschlossen ist, anstatt eine voreingestellte Zeitspanne zu warten. Ihr Test würde ungefähr so aussehen:
CompletableFuture<String> future = new CompletableFuture<>();
executorService.submit(new Runnable() {
@Override
public void run() {
future.complete("Hello World!");
}
});
assertEquals("Hello World!", future.get());
Starten Sie den Prozess und warten Sie mit einem Future
auf das Ergebnis.
Eine Methode, die ich für das Testen von asynchronen Methoden als sehr nützlich empfunden habe, ist das Injizieren 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 als synchron ausgeführt dargestellt werden kann.
Angenommen, ich versuche, die asynchrone Methode Foo#doAsync(Callback c)
zu testen.
class Foo {
private final Executor executor;
public Foo(Executor executor) {
this.executor = executor;
}
public void doAsync(Callback c) {
executor.execute(new Runnable() {
@Override public void run() {
// Do stuff here
c.onComplete(data);
}
});
}
}
In der Produktion würde ich Foo
mit einer Executors.newSingleThreadExecutor()
Executor-Instanz konstruieren, während ich es im Test wahrscheinlich mit einem synchronen Executor konstruieren würde, der Folgendes ausführt:
class SynchronousExecutor implements Executor {
@Override public void execute(Runnable r) {
r.run();
}
}
Jetzt ist mein JUnit-Test der asynchronen Methode ziemlich sauber -
@Test public void testDoAsync() {
Executor executor = new SynchronousExecutor();
Foo objectToTest = new Foo(executor);
Callback callback = mock(Callback.class);
objectToTest.doAsync(callback);
// Verify that Callback#onComplete was called using Mockito.
verify(callback).onComplete(any(Data.class));
// Assert that we got back the data that we expected.
assertEquals(expectedData, callback.getData());
}
Das Testen von Threaded/Async-Code ist von Natur aus nichts Falsches, insbesondere wenn das Threading der Punkt des Codes ist, den Sie testen. Der allgemeine Ansatz zum Testen dieses Materials ist:
Aber das ist viel Heizplatte für einen Test. Ein besserer/einfacherer Ansatz besteht darin, einfach ConcurrentUnit zu verwenden:
final Waiter waiter = new Waiter();
new Thread(() -> {
doSomeWork();
waiter.assertTrue(true);
waiter.resume();
}).start();
// Wait for resume() to be called
waiter.await(1000);
Dies hat gegenüber dem CountdownLatch
-Ansatz den Vorteil, dass es weniger ausführlich ist, da Assertionsfehler, die in einem Thread auftreten, ordnungsgemäß an den Haupt-Thread gemeldet werden, was bedeutet, dass der Test fehlschlägt, wenn er sollte. Eine Beschreibung, die den CountdownLatch
-Ansatz mit ConcurrentUnit vergleicht, lautet hier .
Ich habe auch ein Blogpost zu diesem Thema für diejenigen geschrieben, die etwas mehr Details erfahren möchten.
Wie wäre es, wenn Sie SomeObject.wait
Und notifyAll
wie beschrieben aufrufen hier OR mithilfe von Robotiums Solo.waitForCondition(...)
method OR benutze ein Klasse, die ich geschrieben habe um dies zu tun (siehe Kommentare und Testklasse für die Verwendung)
Es ist erwähnenswert, dass es in Concurrency in Practice ein sehr nützliches Kapitel Testing Concurrent Programs
Gibt, das einige Unit-Test-Ansätze beschreibt und Lösungen für Probleme gibt.
Vermeiden Sie das Testen mit parallelen Threads, wann immer Sie können (was die meiste Zeit der Fall ist). Dadurch werden Ihre Tests nur unvollständig (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 Bibliothek Awaitility anstelle von Thread.sleep()
.
Rufen Sie in Ihren Tests niemals einfach get()
oder join()
auf, da Ihre Tests sonst möglicherweise für immer auf Ihrem CI-Server ausgeführt werden, falls die Zukunft nicht abgeschlossen wird. Setzen Sie in Ihren Tests immer zuerst isDone()
, bevor Sie get()
aufrufen. Für CompletionStage ist dies .toCompletableFuture().isDone()
.
Wenn Sie eine nicht blockierende Methode wie diese testen:
public static CompletionStage<String> createGreeting(CompletableFuture<String> future) {
return future.thenApply(result -> "Hello " + result);
}
dann sollten Sie nicht nur das Ergebnis testen, indem Sie eine abgeschlossene Zukunft im Test übergeben, sondern auch sicherstellen, dass Ihre Methode doSomething()
nicht blockiert, indem Sie join()
oder get()
aufrufen. Dies ist insbesondere dann wichtig, wenn Sie ein nicht blockierendes Framework verwenden.
Testen Sie dazu mit einer nicht abgeschlossenen Zukunft, die Sie manuell abgeschlossen haben:
@Test
public void testDoSomething() throws Exception {
CompletableFuture<String> innerFuture = new CompletableFuture<>();
CompletableFuture<String> futureResult = createGreeting(innerFuture).toCompletableFuture();
assertFalse(futureResult.isDone());
// this triggers the future to complete
innerFuture.complete("world");
assertTrue(futureResult.isDone());
// futher asserts about fooResult here
assertEquals(futureResult.get(), "Hello world");
}
Auf diese Weise schlägt der Test fehl, wenn Sie future.join()
zu doSomething () hinzufügen.
Wenn Ihr Service einen ExecutorService wie in thenApplyAsync(..., executorService)
verwendet, injizieren Sie in Ihren Tests einen ExecutorService mit einem Thread wie den von guava:
ExecutorService executorService = Executors.newSingleThreadExecutor();
Wenn Ihr Code den forkJoinPool wie thenApplyAsync(...)
verwendet, 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 dabei um eine injizierte Referenz, die Sie verspotten würden.
Hier gibt es viele Antworten, aber eine einfache besteht darin, einfach eine fertige CompletableFuture zu erstellen und diese zu verwenden:
CompletableFuture.completedFuture("donzo")
Also in meinem Test:
this.exactly(2).of(mockEventHubClientWrapper).sendASync(with(any(LinkedList.class)));
this.will(returnValue(new CompletableFuture<>().completedFuture("donzo")));
Ich sorge nur dafür, dass das ganze Zeug sowieso angerufen wird. Diese Technik funktioniert, wenn Sie diesen Code verwenden:
CompletableFuture.allOf(calls.toArray(new CompletableFuture[0])).join();
Sobald alle CompletableFutures fertig sind, wird es durch die Datei flitzen!
Ich finde eine Bibliothek socket.io um die asynchrone Logik zu testen. Es sieht einfach und kurz aus mit LinkedBlockingQueue . Hier ist Beispiel :
@Test(timeout = TIMEOUT)
public void message() throws URISyntaxException, InterruptedException {
final BlockingQueue<Object> values = new LinkedBlockingQueue<Object>();
socket = client();
socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() {
@Override
public void call(Object... objects) {
socket.send("foo", "bar");
}
}).on(Socket.EVENT_MESSAGE, new Emitter.Listener() {
@Override
public void call(Object... args) {
values.offer(args);
}
});
socket.connect();
assertThat((Object[])values.take(), is(new Object[] {"hello client"}));
assertThat((Object[])values.take(), is(new Object[] {"foo", "bar"}));
socket.disconnect();
}
Verwenden Sie die LinkedBlockingQueue-API, um zu blockieren, bis das Ergebnis genau wie auf synchrone Weise erhalten wird. Stellen Sie eine Zeitüberschreitung ein, um zu vermeiden, dass Sie zu lange auf das Ergebnis warten müssen.
Ich bevorzuge es zu warten und zu benachrichtigen. Es ist einfach und klar.
@Test
public void test() throws Throwable {
final boolean[] asyncExecuted = {false};
final Throwable[] asyncThrowable= {null};
// do anything async
new Thread(new Runnable() {
@Override
public void run() {
try {
// Put your test here.
fail();
}
// lets inform the test thread that there is an error.
catch (Throwable throwable){
asyncThrowable[0] = throwable;
}
// ensure to release asyncExecuted in case of error.
finally {
synchronized (asyncExecuted){
asyncExecuted[0] = true;
asyncExecuted.notify();
}
}
}
}).start();
// Waiting for the test is complete
synchronized (asyncExecuted){
while(!asyncExecuted[0]){
asyncExecuted.wait();
}
}
// get any async error, including exceptions and assertationErrors
if(asyncThrowable[0] != null){
throw asyncThrowable[0];
}
}
Grundsätzlich müssen wir eine endgültige Array-Referenz erstellen, die innerhalb einer anonymen inneren Klasse verwendet wird. Ich würde lieber einen Booleschen Wert [] erstellen, da ich einen Wert festlegen kann, um zu steuern, ob wir warten müssen (). Wenn alles erledigt ist, geben wir einfach asyncExecuted frei.
Dies ist, was ich heutzutage verwende, wenn das Testergebnis asynchron erzeugt wird.
public class TestUtil {
public static <R> R await(Consumer<CompletableFuture<R>> completer) {
return await(20, TimeUnit.SECONDS, completer);
}
public static <R> R await(int time, TimeUnit unit, Consumer<CompletableFuture<R>> completer) {
CompletableFuture<R> f = new CompletableFuture<>();
completer.accept(f);
try {
return f.get(time, unit);
} catch (InterruptedException | TimeoutException e) {
throw new RuntimeException("Future timed out", e);
} catch (ExecutionException e) {
throw new RuntimeException("Future failed", e.getCause());
}
}
}
Mit statischen Importen liest der Test ein bisschen Nizza. (Beachten Sie, dass ich in diesem Beispiel einen Thread beginne, um die Idee zu veranschaulichen.)
@Test
public void testAsync() {
String result = await(f -> {
new Thread(() -> f.complete("My Result")).start();
});
assertEquals("My Result", result);
}
Ob f.complete
wird nicht aufgerufen, der Test schlägt nach einer Zeitüberschreitung fehl. Sie können auch f.completeExceptionally
früh scheitern.
Wenn Sie die Logik testen möchten, testen Sie sie einfach nicht asynchron.
Zum Beispiel, um diesen Code zu testen, der auf Ergebnissen einer asynchronen Methode arbeitet.
public class Example {
private Dependency dependency;
public Example(Dependency dependency) {
this.dependency = dependency;
}
public CompletableFuture<String> someAsyncMethod(){
return dependency.asyncMethod()
.handle((r,ex) -> {
if(ex != null) {
return "got exception";
} else {
return r.toString();
}
});
}
}
public class Dependency {
public CompletableFuture<Integer> asyncMethod() {
// do some async stuff
}
}
Im Test verspotten Sie die Abhängigkeit mit der synchronen Implementierung. Der Komponententest ist vollständig synchron und dauert 150 ms.
public class DependencyTest {
private Example sut;
private Dependency dependency;
public void setup() {
dependency = Mockito.mock(Dependency.class);;
sut = new Example(dependency);
}
@Test public void success() throws InterruptedException, ExecutionException {
when(dependency.asyncMethod()).thenReturn(CompletableFuture.completedFuture(5));
// When
CompletableFuture<String> result = sut.someAsyncMethod();
// Then
assertThat(result.isCompletedExceptionally(), is(equalTo(false)));
String value = result.get();
assertThat(value, is(equalTo("5")));
}
@Test public void failed() throws InterruptedException, ExecutionException {
// Given
CompletableFuture<Integer> c = new CompletableFuture<Integer>();
c.completeExceptionally(new RuntimeException("failed"));
when(dependency.asyncMethod()).thenReturn(c);
// When
CompletableFuture<String> result = sut.someAsyncMethod();
// Then
assertThat(result.isCompletedExceptionally(), is(equalTo(false)));
String value = result.get();
assertThat(value, is(equalTo("got exception")));
}
}
Sie testen das asynchrone Verhalten nicht, können aber testen, ob die Logik korrekt ist.