web-dev-qa-db-ja.com

JUnitを使用して非同期プロセスをテストする方法

JUnitで非同期プロセスを起動するメソッドをどのようにテストしますか?

プロセスが終了するまでテストを待機させる方法がわかりません(厳密には単体テストではなく、1つだけではなく複数のクラスが関係するため、統合テストに似ています)。

177
Sam

私見では、単体テストでスレッドなどを作成したり、待機させたりするのは悪い習慣です。これらのテストを数秒で実行したいと思います。だからこそ、非同期プロセスをテストする2段階のアプローチを提案したいと思います。

  1. 非同期プロセスが適切に送信されることをテストします。非同期リクエストを受け入れるオブジェクトをモックし、送信されたジョブに正しいプロパティなどがあることを確認できます。
  2. 非同期コールバックが正しいことを行っていることをテストします。ここで、最初に送信されたジョブをモックアウトし、それが適切に初期化されていると想定して、コールバックが正しいことを確認できます。
43
Cem Catikkas

別の方法は、 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の周りの同期ブロックを削除

179
Martin

Awaitility ライブラリを使用してみてください。これにより、話しているシステムを簡単にテストできます。

65
Johan

CompletableFuture (Java 8で導入)または SettableFutureGoogle Guava から)を使用すると、事前に設定された時間待機するのではなく、テストが完了するとすぐにテストが終了します。テストは次のようになります。

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

プロセスを開始し、 Future を使用して結果を待ちます。

22

非同期メソッドのテストに非常に役立つとわかったメソッドの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());
}
17
Matt

スレッド化/非同期コードのテストに本質的に問題はありません。特に、スレッド化がテストするコードのポイントである場合。このようなものをテストする一般的なアプローチは次のとおりです。

  • メインテストスレッドをブロックする
  • 他のスレッドから失敗したアサーションをキャプチャします
  • メインテストスレッドのブロックを解除する
  • 失敗を再スロー

しかし、それは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 です。

また、このトピックについて ブログ投稿 を書いて、もう少し詳しく学びたい人向けに書いた。

5
Jonathan

説明どおりにSomeObject.waitおよびnotifyAllを呼び出してはどうですか here OR usingRobotiumsSolo.waitForCondition(...)メソッドORを使用して クラスを書いた を実行します(使用方法についてはコメントとテストクラスを参照してください)

4
Dori

Concurrency in Practice には非常に便利なTesting Concurrent Programsの章があることに言及する価値があります。

3
eleven

できる限り並列スレッドでテストすることは避けてください(ほとんどの場合)。これにより、テストが不安定になります(パスすることもあれば、失敗することもあります)。

他のライブラリ/システムを呼び出す必要がある場合にのみ、他のスレッドで待機する必要があります。その場合は、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にしました。通常は、模擬参照を挿入します。

2
tkruse

ここには多くの答えがありますが、簡単なものは、完成した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がすぐに圧縮されます。

2
markthegrea

非同期ロジックをテストするためのライブラリ 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が使用されます。また、タイムアウトを設定して、結果を待つ時間が長すぎると想定しないようにします。

1
Fantasy Fang

待機して通知することを好みます。シンプルで明確です。

@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をリリースします。

1
Paulo

テスト結果が非同期に生成される場合、これは私が最近使用しているものです。

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を使用して、早期に失敗することもできます。

1

ロジックをテストする場合は、非同期でテストしないでください。

たとえば、非同期メソッドの結果で機能するこのコードをテストするには。

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

非同期の動作はテストしませんが、ロジックが正しいかどうかをテストできます。

0
Nils El-Himoud