JUnitで非同期プロセスを起動するメソッドをどのようにテストしますか?
プロセスが終了するまでテストを待機させる方法がわかりません(厳密には単体テストではなく、1つだけではなく複数のクラスが関係するため、統合テストに似ています)。
私見では、単体テストでスレッドなどを作成したり、待機させたりするのは悪い習慣です。これらのテストを数秒で実行したいと思います。だからこそ、非同期プロセスをテストする2段階のアプローチを提案したいと思います。
別の方法は、 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通常のオブジェクトをロックとして使用することはできませんsyncronized、高速コールバックは前にロックを解除できるためロックの待機メソッドが呼び出されます。 this Joe Walnesによるブログ投稿を参照してください。
EDIT@jtahlbornと@RingからのコメントのおかげでCountDownLatchの周りの同期ブロックを削除
Awaitility ライブラリを使用してみてください。これにより、話しているシステムを簡単にテストできます。
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());
プロセスを開始し、 Future
を使用して結果を待ちます。
非同期メソッドのテストに非常に役立つとわかったメソッドの1つは、テストするオブジェクトのコンストラクターにExecutor
インスタンスを挿入することです。実稼働環境では、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()
エグゼキューターインスタンスで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());
}
スレッド化/非同期コードのテストに本質的に問題はありません。特に、スレッド化がテストするコードのポイントである場合。このようなものをテストする一般的なアプローチは次のとおりです。
しかし、それは1つのテストには多くの決まり文句です。より良い/簡単なアプローチは、単に 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を比較する記事は here です。
また、このトピックについて ブログ投稿 を書いて、もう少し詳しく学びたい人向けに書いた。
Concurrency in Practice には非常に便利なTesting Concurrent Programs
の章があることに言及する価値があります。
できる限り並列スレッドでテストすることは避けてください(ほとんどの場合)。これにより、テストが不安定になります(パスすることもあれば、失敗することもあります)。
他のライブラリ/システムを呼び出す必要がある場合にのみ、他のスレッドで待機する必要があります。その場合は、Thread.sleep()
の代わりに Awaitility ライブラリを常に使用してください。
テストでget()
またはjoin()
を呼び出さないでください。そうしないと、将来が完了しない場合に備えてCIサーバーでテストが永久に実行される可能性があります。 isDone()
を呼び出す前に、必ずテストでget()
を最初にアサートしてください。 CompletionStageの場合は、.toCompletableFuture().isDone()
です。
このような非ブロッキングメソッドをテストする場合:
public static CompletionStage<String> createGreeting(CompletableFuture<String> future) {
return future.thenApply(result -> "Hello " + result);
}
次に、テストで完了したFutureを渡すことで結果をテストするだけでなく、doSomething()
またはjoin()
を呼び出してメソッドget()
がブロックされないことも確認する必要があります。これは、特にノンブロッキングフレームワークを使用する場合に重要です。
これを行うには、手動で完了に設定した未完了のフューチャーでテストします。
@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");
}
そうすれば、future.join()
をdoSomething()に追加すると、テストは失敗します。
サービスがthenApplyAsync(..., executorService)
などのExecutorServiceを使用している場合、テストでguavaのようなシングルスレッドのExecutorServiceを挿入します。
ExecutorService executorService = Executors.newSingleThreadExecutor();
コードでthenApplyAsync(...)
などのforkJoinPoolを使用する場合は、ExecutorServiceを使用するようにコードを書き換える(多くの理由がある)か、Awaitilityを使用します。
この例を短くするために、テストでJava8ラムダとして実装されたメソッド引数をBarServiceにしました。通常は、模擬参照を挿入します。
ここには多くの答えがありますが、簡単なものは、完成したCompletableFutureを作成して使用することです。
CompletableFuture.completedFuture("donzo")
だから私のテストでは:
this.exactly(2).of(mockEventHubClientWrapper).sendASync(with(any(LinkedList.class)));
this.will(returnValue(new CompletableFuture<>().completedFuture("donzo")));
とにかく、これらすべてのものが呼び出されることを確認しています。この手法は、次のコードを使用している場合に機能します。
CompletableFuture.allOf(calls.toArray(new CompletableFuture[0])).join();
すべてのCompletableFutureが終了すると、Zipがすぐに圧縮されます。
非同期ロジックをテストするためのライブラリ socket.io を見つけました。 LinkedBlockingQueue を使用すると、簡単で簡単な方法に見えます。 例 です:
@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が使用されます。また、タイムアウトを設定して、結果を待つ時間が長すぎると想定しないようにします。
待機して通知することを好みます。シンプルで明確です。
@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];
}
}
基本的に、匿名の内部クラスの内部で使用される最終的な配列参照を作成する必要があります。 wait()が必要かどうかを制御する値を設定できるので、boolean []を作成したいです。すべてが完了したら、asyncExecutedをリリースします。
テスト結果が非同期に生成される場合、これは私が最近使用しているものです。
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
を使用して、早期に失敗することもできます。
ロジックをテストする場合は、非同期でテストしないでください。
たとえば、非同期メソッドの結果で機能するこのコードをテストするには。
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
}
}
テストでは、同期実装を使用して依存関係を模擬します。単体テストは完全に同期しており、150msで実行されます。
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")));
}
}
非同期の動作はテストしませんが、ロジックが正しいかどうかをテストできます。