JUnitで非同期プロセスを起動するメソッドをどのようにテストしますか?
プロセスが終了するまでテストを待機させる方法がわかりません(これは厳密に単体テストではなく、1つだけではなく複数のクラスが含まれるため、統合テストのようなものです)。
JUnitで非同期プロセスを起動するメソッドをどのようにテストしますか?
プロセスが終了するまでテストを待機させる方法がわかりません(これは厳密に単体テストではなく、1つだけではなく複数のクラスが含まれるため、統合テストのようなものです)。
回答:
IMHO単体テストでスレッドを作成したりスレッドで待機したりすることは悪い習慣です。これらのテストを数秒で実行したいとします。これが、非同期プロセスをテストするための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ロックの待機メソッドが呼び出される前に、高速のコールバックがロックを解除することができますよう、ロックなどの通常のオブジェクトと。Joe Walnesによるこのブログ投稿を参照してください。
編集 @jtahlbornと@RingからのコメントのおかげでCountDownLatchの周りの同期ブロックを削除しました
Awaitilityライブラリーを使用してみてください。それはあなたが話しているシステムをテストすることを簡単にします。
CountDownLatch
、この点では(@Martinの回答を参照)を使用する方が良いと思います。
あなたが使用している場合はCompletableFuture(Javaの8で導入)やSettableFuture(からGoogleのグアバを)、あなたはかなりの時間の予め設定された量を待っているよりも、すぐにそれを行うのと同様に、あなたのテスト仕上げを行うことができます。テストは次のようになります。
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
するオブジェクトのコンストラクターにインスタンスをです。本番環境では、エグゼキューターインスタンスは非同期で実行されるように構成されていますが、テストでは、モックして同期して実行できます。
だから私が非同期メソッドをテストしようとしているとしましょう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);
}
});
}
}
本番環境Foo
では、Executors.newSingleThreadExecutor()
Executorインスタンスを使用して構築しますが、テストでは、以下を実行する同期エグゼキューターを使用して構築します-
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());
}
WebClient
スレッド化/非同期コードのテストに本質的に問題はありません。特に、テストするコードのポイントがスレッド化である場合はそうです。これをテストする一般的な方法は次のとおりです。
しかし、これは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へのアプローチを比較した記事はこちらです。
もう少し詳しく知りたい人のために、このトピックに関するブログ投稿も書きました。
呼び出してSomeObject.wait
、ここでnotifyAll
説明されているように、またはRobotiums Solo.waitForCondition(...)
メソッドを使用するか、これを行うために書いたクラスを使用します(使用方法については、コメントとテストクラスを参照してください)
非同期ロジックをテストするための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が使用されます。そして、タイムアウトを設定して、結果を待つ時間が長すぎると想定しないようにします。
いくつかの単体テストアプローチを説明し、問題の解決策を提供するTesting Concurrent Programs
、Concurrency in Practiceの非常に便利な章があることは言及する価値があります。
これは、テスト結果が非同期で生成される場合、私が最近使用しているものです。
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
早く失敗するのにも使えます。
ここには多くの答えがありますが、簡単なのは、完成した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が完了すると、それはすぐにジップします!
できる限り(ほとんどの場合)、並列スレッドでのテストは避けてください。これにより、テストが不安定になります(場合によっては成功し、失敗することもあります)。
他のライブラリ/システムを呼び出す必要がある場合のみ、他のスレッドで待機する必要がある場合があります。その場合は、常に代わりにAwaitilityライブラリを使用してください。Thread.sleep()
。
テストを呼び出し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()に追加すると、テストは失敗します。
サービスでのようなExecutorServiceを使用している場合、thenApplyAsync(..., executorService)
テストでは、guavaのサービスなど、シングルスレッドのExecutorServiceを挿入します。
ExecutorService executorService = Executors.newSingleThreadExecutor();
コードでなどのforkJoinPool thenApplyAsync(...)
を使用する場合は、ExecutorServiceを使用するようにコードを書き直すか(多くの理由があります)、またはAwaitilityを使用します。
例を短くするために、テストではJava8ラムダとして実装されたメソッド引数をBarServiceにしました。通常、これは、モックする注入された参照です。
私は待機と通知を使用することを好みます。シンプルでクリアです。
@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];
}
}
基本的に、匿名の内部クラスの内部で使用するために、最後の配列参照を作成する必要があります。むしろboolean []を作成します。これは、wait()が必要かどうかを制御する値を設定できるためです。すべてが完了したら、asyncExecutedを解放するだけです。
そこにいるすべてのSpringユーザーにとって、これは今日の通常の統合テスト方法であり、非同期動作が関係しています。
非同期タスク(I / O呼び出しなど)が完了したときに、製品コードでアプリケーションイベントを発生させます。ほとんどの場合、本番環境で非同期操作の応答を処理するには、このイベントが必要です。
このイベントを配置すると、テストケースで次の戦略を使用できます。
これを分解するには、最初に何らかのドメインイベントを発生させる必要があります。ここでは、完了したタスクを識別するためにUUIDを使用していますが、固有のものである限り、他のものを使用してもかまいません。
(次のコードスニペットもLombokアノテーションを使用してボイラープレートコードを取り除くことに注意してください)
@RequiredArgsConstructor
class TaskCompletedEvent() {
private final UUID taskId;
// add more fields containing the result of the task if required
}
量産コード自体は通常、次のようになります。
@Component
@RequiredArgsConstructor
class Production {
private final ApplicationEventPublisher eventPublisher;
void doSomeTask(UUID taskId) {
// do something like calling a REST endpoint asynchronously
eventPublisher.publishEvent(new TaskCompletedEvent(taskId));
}
}
次に、Spring @EventListener
を使用して、公開されたイベントをテストコードでキャッチできます。イベントリスナーは、スレッドセーフな方法で2つのケースを処理する必要があるため、少し複雑です。
CountDownLatch
ここで他の回答で述べられているように、A は2番目のケースに使用されます。また@Order
、イベントハンドラーメソッドのアノテーションは、このイベントハンドラーメソッドが、本番環境で使用される他のイベントリスナーの後に呼び出されることを確認します。
@Component
class TaskCompletionEventListener {
private Map<UUID, CountDownLatch> waitLatches = new ConcurrentHashMap<>();
private List<UUID> eventsReceived = new ArrayList<>();
void waitForCompletion(UUID taskId) {
synchronized (this) {
if (eventAlreadyReceived(taskId)) {
return;
}
checkNobodyIsWaiting(taskId);
createLatch(taskId);
}
waitForEvent(taskId);
}
private void checkNobodyIsWaiting(UUID taskId) {
if (waitLatches.containsKey(taskId)) {
throw new IllegalArgumentException("Only one waiting test per task ID supported, but another test is already waiting for " + taskId + " to complete.");
}
}
private boolean eventAlreadyReceived(UUID taskId) {
return eventsReceived.remove(taskId);
}
private void createLatch(UUID taskId) {
waitLatches.put(taskId, new CountDownLatch(1));
}
@SneakyThrows
private void waitForEvent(UUID taskId) {
var latch = waitLatches.get(taskId);
latch.await();
}
@EventListener
@Order
void eventReceived(TaskCompletedEvent event) {
var taskId = event.getTaskId();
synchronized (this) {
if (isSomebodyWaiting(taskId)) {
notifyWaitingTest(taskId);
} else {
eventsReceived.add(taskId);
}
}
}
private boolean isSomebodyWaiting(UUID taskId) {
return waitLatches.containsKey(taskId);
}
private void notifyWaitingTest(UUID taskId) {
var latch = waitLatches.remove(taskId);
latch.countDown();
}
}
最後のステップは、テストケースでテスト中のシステムを実行することです。ここではJUnit 5でSpringBootテストを使用していますが、これはSpringコンテキストを使用するすべてのテストで同じように機能するはずです。
@SpringBootTest
class ProductionIntegrationTest {
@Autowired
private Production sut;
@Autowired
private TaskCompletionEventListener listener;
@Test
void thatTaskCompletesSuccessfully() {
var taskId = UUID.randomUUID();
sut.doSomeTask(taskId);
listener.waitForCompletion(taskId);
// do some assertions like looking into the DB if value was stored successfully
}
}
ここでの他の回答とは異なり、このソリューションは、テストを並行して実行し、複数のスレッドが同時に非同期コードを実行する場合にも機能することに注意してください。
ロジックをテストする場合は、非同期でテストしないでください。
たとえば、非同期メソッドの結果で機能するこのコードをテストします。
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")));
}
}
非同期動作はテストしませんが、ロジックが正しいかどうかをテストできます。