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


204

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

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


JAT(Java Asynchronous Test)を試すことができます:bitbucket.org/csolar/jat
cs0lar

2
JATのウォッチャーは1人で、1.5年間更新されていません。Awaitilityは1か月前に更新され、この記事の執筆時点ではバージョン1.6になっています。私はどちらのプロジェクトにも所属していませんが、自分のプロジェクトに追加で投資するつもりであれば、現時点ではAwaitilityをより信頼できると思います。
Les Hazlewood 2014

JATにはまだ更新がありません:「最終更新2013-01-19」。リンクをたどる時間を節約するだけです。
デーモン

@ LesHazlewood、1人のウォッチャーはJATに悪いですが、何年も更新されていません...ほんの一例です。それが機能する場合、OSの低レベルTCPスタックをどれくらいの頻度で更新しますか?JATの代替案は、stackoverflow.com / questions / 631598 /…の下で回答されています。
user1742529

回答:


46

IMHO単体テストでスレッドを作成したりスレッドで待機したりすることは悪い習慣です。これらのテストを数秒で実行したいとします。これが、非同期プロセスをテストするための2段階のアプローチを提案する理由です。

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

148
承知しました。ただし、スレッドを管理するためのコードをテストする必要がある場合もあります。
11年

77
JunitまたはTestNGを使用して統合テスト(単体テストだけでなく)またはユーザー受け入れテスト(Cucumberなど)を行う場合、非同期の完了を待ち、結果を確認することが絶対に必要です。
Les Hazlewood 2014

38
非同期プロセスは正しく理解するための最も複雑なコードの一部であり、それらに対してユニットテストを使用してはならず、単一スレッドでのみテストするべきだとおっしゃっていますか?それは非常に悪い考えです。
Charles

18
モックテストでは、機能がエンドツーエンドで機能することを証明できないことがよくあります。非同期機能は、機能することを確認するために非同期でテストする必要があります。必要に応じてこれを統合テストと呼びますが、それはまだ必要なテストです。
Scott Boring

4
これは受け入れられる答えではありません。テストは単体テストを超えています。OPは、単体テストというよりも統合テストのように呼びかけています。
エレミヤアダムス

190

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


8
この例には従わないでください。間違っています。スレッドセーフティを内部的に処理するため、CountDownLatchで同期しないでください。
jtahlborn 2012年

1
同期された部分までは、おそらく3〜4時間のデバッグ時間を費やすことになるので、良いアドバイスでした。stackoverflow.com/questions/11007551/…–
リング

2
エラーの謝罪。答えを適切に編集しました。
マーティン

7
onSuccessが呼び出されたことを確認している場合は、lock.awaitがtrueを返すことをアサートする必要があります。
ギルバート

1
@Martinは正解ですが、修正が必要な別の問題があることを意味します。
ジョージAristy

75

Awaitilityライブラリーを使用してみてください。それはあなたが話しているシステムをテストすることを簡単にします。


21
フレンドリーな免責事項:Johanはプロジェクトの主な貢献者です。
dbm '18

1
待機なければならないという基本的な問題に苦しんでいます(単体テストは高速に実行する必要があります)。理想的には、必要以上に1ミリ秒も待機したくないのでCountDownLatch、この点では(@Martinの回答を参照)を使用する方が良いと思います。
ジョージAristy

本当に素晴らしい。
R. Karlus

これは、私の非同期プロセス統合テスト要件を満たす完全なライブラリです。本当に素晴らしい。ライブラリはよくメンテナンスされているようで、基本的な機能から高度な機能まであり、ほとんどのシナリオに対応するには十分だと思います。素晴らしいリファレンスをありがとう!
Tanvir

本当に素晴らしい提案です。ありがとう
RoyalTiger

68

あなたが使用している場合は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());

4
...そしてもしあなたしているのjava-より少なくより8トライグアバのにこだわってSettableFutureほとんど同じことをする
マルクスT


18

非同期メソッドをテストするのにかなり便利だと思った方法の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());
}

Springのような非同期ライブラリ呼び出しを含む何かを統合テストしたい場合は機能しませんWebClient
Stefan Haberl

8

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

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

しかし、これは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アプローチに対するこれの利点は、任意のスレッドで発生するアサーションエラーがメインスレッドに適切に報告されるため、冗長性が低くなることです。つまり、テストは必要なときに失敗します。CountdownLatchConcurrentUnitへのアプローチを比較した記事はこちらです。

もう少し詳しく知りたい人のために、このトピックに関するブログ投稿も書きました。


私が過去に使用した同様のソリューションはgithub.com/MichaelTamm/junit-toolboxです。これはjunit.org / junit4のサードパーティ拡張機能としても機能します
dschulten

4

呼び出してSomeObject.waitここでnotifyAll説明されているように、またはRobotiums Solo.waitForCondition(...)メソッドを使用するか、これを行うために書いクラスを使用します(使用方法については、コメントとテストクラスを参照してください)


1
待機/通知/割り込みのアプローチの問題は、テストしているコードが待機中のスレッドに干渉する可能性があることです(私はそれが発生するのを見てきました)。これが、ConcurrentUnitがスレッドが待機できるプライベート回路を使用する理由です。この回路は、メインテストスレッドへの割り込みによって誤って干渉されることはありません。
ジョナサン

3

非同期ロジックをテストするための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
素晴らしいアプローチ!
アミノグラフィー

3

いくつかの単体テストアプローチを説明し、問題の解決策を提供するTesting Concurrent ProgramsConcurrency in Practiceの非常に便利な章があることは言及する価値があります。


1
それはどのアプローチですか?例を挙げていただけますか?
ブルーノフェレイラ

2

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

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早く失敗するのにも使えます。


2

ここには多くの答えがありますが、簡単なのは、完成した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が完了すると、それはすぐにジップします!


2

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

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


@tkruseさん、このテクニックを使用したテストを備えた公開gitリポジトリがありますか?
クリスティアーノ

@クリスティアーノ:それはSOの哲学に反するだろう。代わりに、メソッドを空のjunitテストクラスに貼り付けたときに、追加のコードなしでコンパイルするようにメソッドを変更しました(すべてのインポートはjava8 +またはjunitです)。遠慮なく賛成投票してください。
tkruse

私は今理解しました。ありがとう。ここでの問題は、メソッドがCompletableFutureを返すときにテストすることですが、CompletableFuture以外のパラメーターとして他のオブジェクトを受け入れます。
クリスティアーノ

あなたの場合、メソッドが返すCompletableFutureを作成するのは誰ですか?それが別のサービスである場合、それはあざけることができ、私のテクニックはまだ適用されます。メソッド自体がCompletableFutureを作成する場合、状況は非常に変化するので、それについて新しい質問をすることができます。次に、メソッドが返す未来をどのスレッドが完了するかによって異なります。
tkruse

1

私は待機と通知を使用することを好みます。シンプルでクリアです。

@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を解放するだけです。


1
アサーションが失敗した場合、メインテストスレッドはそれを認識しません。
ジョナサン

解決策をありがとう、websocket接続でコードをデバッグするのに役立ちます
Nazar Sakharenko、2016

@Jonathan、アサーションと例外をキャッチしてメインテストスレッドに通知するようにコードを更新しました。
Paulo

1

そこにいるすべてのSpringユーザーにとって、これは今日の通常の統合テスト方法であり、非同期動作が関係しています。

非同期タスク(I / O呼び出しなど)が完了したときに、製品コードでアプリケーションイベントを発生させます。ほとんどの場合、本番環境で非同期操作の応答を処理するには、このイベントが必要です。

このイベントを配置すると、テストケースで次の戦略を使用できます。

  1. テスト中のシステムを実行する
  2. イベントをリッスンして、イベントが発生したことを確認します
  3. あなたの主張をする

これを分解するには、最初に何らかのドメインイベントを発生させる必要があります。ここでは、完了したタスクを識別するために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つのケースを処理する必要があるため、少し複雑です。

  1. 本番コードはテストケースよりも高速で、テストケースがイベントをチェックする前にイベントがすでに発生している、または
  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
  }

}

ここでの他の回答とは異なり、このソリューションは、テストを並行して実行し、複数のスレッドが同時に非同期コードを実行する場合にも機能することに注意してください。


0

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

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

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.