[java] 如何使用Junit來測試異步進程


Answers

另一種方法是使用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());
    }
}

注意 ,不能僅將與常規對象syncronized用作鎖,因為快速回調可以在調用鎖的等待方法之前釋放鎖。 請參閱Joe Walnes的博客文章。

編輯刪除CountDownLatch周圍的同步塊,感謝來自@jtahlborn和@Ring的評論

Question

你如何測試使用Junit激發異步過程的方法?

我不知道如何讓我的測試等待進程結束(這不完全是一個單元測試,它更像是一個集成測試,因為它涉及幾個類而不只是一個)




測試線程/異步代碼沒有任何內在的錯誤,尤其是線程是您正在測試的代碼的重點 。 測試這些東西的一般方法是:

  • 阻止主測試線程
  • 從其他線程捕獲失敗的斷言
  • 解鎖主測試線程
  • 反思任何失敗

但這是一個測試的很多樣板。 更好/更簡單的方法是使用ConcurrentUnit

  final Waiter waiter = new Waiter();

  new Thread(() -> {
    doSomeWork();
    waiter.assertTrue(true);
    waiter.resume();
  }).start();

  // Wait for resume() to be called
  waiter.await(1000);

CountdownLatch方法相比,這樣做的好處在於它不那麼冗長,因為任何線程中發生的斷言失敗都會​​正確地報告給主線程,這意味著測試在應該執行時會失敗。 將CountdownLatch方法與ConcurrentUnit進行比較的CountdownLatchhere

我還為那些想要了解更多細節的人寫了here關於該主題的here




我找到一個庫socket.io來測試異步邏輯。 它使用LinkedBlockingQueue看起來簡單而簡單。 這裡是一個example

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

使用LinkedBlockingQueue將API阻塞,直到像同步方式一樣獲得結果。 並設置超時以避免假設有太多時間來等待結果。




這是我現在使用的測試結果是異步生成的。

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

使用靜態導入,測試讀起來不錯。 (注意,在這個例子中,我開始一個線索來說明這個想法)

    @Test
    public void testAsync() {
        String result = await(f -> {
            new Thread(() -> f.complete("My Result")).start();
        });
        assertEquals("My Result", result);
    }

如果未調用f.complete ,則在超時後測試將失敗。 您也可以使用f.completeExceptionally失敗。




盡可能避免使用並行線程進行測試(這是大部分時間)。 這只會讓你的測試失敗(有時候通過,有時會失敗)。

只有當你需要調用其他庫/系統時,你可能需要等待其他線程,在這種情況下,總是使用Awaitility庫而不是Thread.sleep()

切勿在測試中調用get()join() ,否則您的測試可能會永久運行在您的CI服務器上,以防將來永遠無法完成。 在調用get()之前,始終在測試中首先聲明isDone() get() 。 對於CompletionStage,即.toCompletableFuture().isDone()

當你測試這樣的非阻塞方法時:

public static CompletionStage<Foo> doSomething(BarService service) {
    CompletionStage<Bar> future = service.getBar();
    return future.thenApply(bar -> fooToBar());
}

那麼你不應該僅僅通過在測試中傳遞完整的Future來測試結果,還應該確保你的方法doSomething()不會通過調用join()get()阻塞。 如果您使用非阻塞框架,這一點尤為重要。

為此,請測試您設置為手動完成的未完成未來:

@Test
public void testDoSomething() {
    CompletableFuture<Bar> innerFuture = new CompletableFuture<>();
    fooResult = doSomething(() -> innerFuture).toCompletableFuture();
    assertFalse(fooResult.isDone());

    // this triggers the future to complete
    innerFuture.complete(new Bar());
    assertTrue(fooResult.isDone());

    // futher asserts about fooResult here
}

這樣,如果將future.join()添加到doSomething(),則測試將失敗。

如果你的服務使用了一個ExecutorService,比如在thenApplyAsync(..., executorService) ,那麼在你的測試中註入一個單線程的ExecutorService,比如來自guava的ExecutorService:

ExecutorService executorService = Executors.newSingleThreadExecutor();

如果您的代碼使用thenApplyAsync(...)類的thenApplyAsync(...) ,請重寫代碼以使用ExecutorService(有很多很好的理由),或者使用Awaitility。

為了縮短這個例子,我在測試中將BarService作為一個Java8 lambda實現的方法參數,通常它會是一個你會模擬的注入引用。




我發現一種​​非常有用的測試異步方法的方法是在對象測試的構造函數中註入一個Executor實例。 在生產中,執行程序實例被配置為異步運行,而在測試中它可以被模擬為同步運行。

所以假設我試圖測試異步方法Foo#doAsync(Callback c)

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);
      }
    });
  }
}

在生產中,我會用Executors.newSingleThreadExecutor() Executor實例構建Foo ,而在測試中我可能會用一個同步執行程序來構建它,它執行以下操作 -

class SynchronousExecutor implements Executor {
  @Override public void execute(Runnable r) {
    r.run();
  }
}

現在我對異步方法的JUnit測試非常乾淨 -

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



如果您使用CompletableFuture (在Java 8中引入)或SettableFuture (來自Google Guava ),則可以在完成測試後立即完成測試,而不是等待預設的時間量。 你的測試看起來像這樣:

CompletableFuture<String> future = new CompletableFuture<>();
executorService.submit(new Runnable() {         
    @Override
    public void run() {
        future.complete("Hello World!");                
    }
});
assertEquals("Hello World!", future.get());



值得一提的是,在並發實踐中有非常有用的章節Testing Concurrent Programs ,它描述了一些單元測試方法並給出了問題的解決方案。






Related