it-swarm.com.de

Verwendung von JUnit zum Testen asynchroner Prozesse

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).

177
Sam

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.

  1. Testen Sie, ob Ihr Async-Prozess ordnungsgemäß übermittelt wurde. Sie können das Objekt verspotten, das Ihre asynchronen Anforderungen akzeptiert, und sicherstellen, dass der übergebene Job über die richtigen Eigenschaften verfügt.
  2. Testen Sie, ob Ihre asynchronen Rückrufe die richtigen Aktionen ausführen. Hier können Sie den ursprünglich gesendeten Job verspotten und davon ausgehen, dass er ordnungsgemäß initialisiert wurde, und überprüfen, ob Ihre Rückrufe korrekt sind.
43
Cem Catikkas

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

179
Martin

Sie können versuchen, die Bibliothek Awaitility zu verwenden. Es macht es einfach, die Systeme zu testen, über die Sie sprechen.

65
Johan

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());
60
user393274

Starten Sie den Prozess und warten Sie mit einem Future auf das Ergebnis.

22

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());
}
17
Matt

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:

  • Blockieren Sie den Haupttest-Thread
  • Erfassen Sie fehlgeschlagene Zusicherungen aus anderen Threads
  • Entsperren Sie den Haupttest-Thread
  • Werfen Sie alle Fehler erneut

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.

5
Jonathan

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)

4
Dori

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.

3
eleven

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.

2
tkruse

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!

2
markthegrea

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.

1
Fantasy Fang

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.

1
Paulo

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.

1

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.

0
Nils El-Himoud