14

android.os.Handler クラスを使用して、バックグラウンドでタスクを実行します。これらを単体テストするときLooper.loop()は、テスト スレッドがバックグラウンド タスク スレッドの処理を待機するように呼び出します。後で、Looper.myLooper().quit()(テスト スレッドでも) を呼び出して、テスト スレッドが を終了しloop、テスト ロジックを再開できるようにします。

複数のテストメソッドを書きたいと思うまでは、すべてうまくいきます。

問題は、Looper が同じスレッドでの終了と再起動を許可するように設計されていないように見えることです。そのため、単一のテスト メソッド内ですべてのテストを行う必要があります。

Looper のソース コードを調べたところ、回避策が見つかりませんでした。

自分の Hander/Looper コードをテストする他の方法はありますか? それとも、バックグラウンド タスク クラスを作成するための、よりテストに適した方法でしょうか。

4

4 に答える 4

3

AsyncTaskこれは、2番目のルーパースレッド(Androidによって暗黙的に作成されたメインスレッド以外)を導入することで、使用していない場合でも実際に簡単にテストできます。基本的な戦略は、CountDownLatchすべてのコールバックを2番目のルーパースレッドに委任している間、メインルーパースレッドをブロックすることです。

ここでの注意点は、テスト対象のコードがデフォルトのメインループ以外のルーパーの使用をサポートできる必要があるということです。より堅牢で柔軟な設計をサポートするかどうかに関係なく、これが当てはまるはずであり、幸いにも非常に簡単であると私は主張します。一般に、実行する必要があるのは、オプションのLooperパラメーターを受け入れるようにコードを変更し、それを使用してHandler(as new Handler(myLooper))を作成することだけです。の場合AsyncTask、この要件により、このアプローチでテストすることは不可能になります。私が思う問題はそれ自体で解決されるべきだと思いますAsyncTask

開始するためのサンプルコード:

public void testThreadedDesign() {
    final CountDownLatch latch = new CountDownLatch(1);

    /* Just some class to store your result. */
    final TestResult result = new TestResult(); 

    HandlerThread testThread = new HandlerThread("testThreadedDesign thread");
    testThread.start();

    /* This begins a background task, say, doing some intensive I/O.
     * The listener methods are called back when the job completes or
     * fails. */
    new ThingThatOperatesInTheBackground().doYourWorst(testThread.getLooper(),
            new SomeListenerThatTotallyShouldExist() {
        public void onComplete() {
            result.success = true;
            finished();
        }

        public void onFizzBarError() {
            result.success = false;
            finished();
        }

        private void finished() {
            latch.countDown();
        }
    });

    latch.await();

    testThread.getLooper().quit();

    assertTrue(result.success);
}
于 2012-12-12T02:43:04.457 に答える
3

Looper のソース コードは、Looper.myLooper().quit() がメッセージ キューにヌル メッセージをエンキューすることを示しています。基本的に、スレッドはその時点でデッドスレッドになり、私が知っているスレッドを復活させる方法はありません。「死んだスレッドにメッセージを送信しようとしています」という効果で quit() が呼び出された後に、メッセージをハンドラーに投稿しようとすると、エラー メッセージが表示される場合があります。それが意味することです。

于 2011-07-01T00:54:08.047 に答える
0

@Josh Guilfoyleの答えに触発されて、リフレクションを使用して、独自の非ブロックおよび非終了を作成するために必要なものにアクセスすることにしましたLooper.loop()

/**
 * Using reflection, steal non-visible "message.next"
 * @param message
 * @return
 * @throws Exception
 */
private Message _next(Message message) throws Exception {
    Field f = Message.class.getDeclaredField("next");
    f.setAccessible(true);
    return (Message)f.get(message);
}

/**
 * Get and remove next message in local thread-pool. Thread must be associated with a Looper.
 * @return next Message, or 'null' if no messages available in queue.
 * @throws Exception
 */
private Message _pullNextMessage() throws Exception {
    final Field _messages = MessageQueue.class.getDeclaredField("mMessages");
    final Method _next = MessageQueue.class.getDeclaredMethod("next");

    _messages.setAccessible(true);
    _next.setAccessible(true);

    final Message root = (Message)_messages.get(Looper.myQueue());
    final boolean wouldBlock = (_next(root) == null);
    if(wouldBlock)
        return null;
    else
        return (Message)_next.invoke(Looper.myQueue());
}

/**
 * Process all pending Messages (Handler.post (...)).
 * 
 * A very simplified version of Looper.loop() except it won't
 * block (returns if no messages available). 
 * @throws Exception
 */
private void _doMessageQueue() throws Exception {
    Message msg;
    while((msg = _pullNextMessage()) != null) {
        msg.getTarget().dispatchMessage(msg);
    }
}

これで、テスト (UI スレッドで実行する必要があります) で、次のことができるようになりました。

@UiThreadTest
public void testCallbacks() throws Throwable {
    adapter = new UpnpDeviceArrayAdapter(getInstrumentation().getContext(), upnpService);

    assertEquals(0, adapter.getCount());

    upnpService.getRegistry().addDevice(createRemoteDevice());
    // the adapter posts a Runnable which adds the new device.
    // it has to because it must be run on the UI thread. So we
    // so we need to process this (and all other) handlers before
    // checking up on the adapter again.
    _doMessageQueue();

    assertEquals(2, adapter.getCount());

    // remove device, _doMessageQueue() 
}

これが良いアイデアだと言っているわけではありませんが、これまでのところうまくいっています。試してみる価値ありかも!これについて私が気に入っているのは、Exceptions一部の内部でスローhander.post(...)されるとテストが中断されることです。それ以外の場合はそうではありません。

于 2013-01-21T12:49:18.480 に答える
0

I've stumbled in the same issue as yours. I also wanted to make a test case for a class that use a Handler.

Same as what you did, I use the Looper.loop() to have the test thread starts handling the queued messages in the handler.

To stop it, I used the implementation of MessageQueue.IdleHandler to notify me when the looper is blocking to wait the next message to come. When it happen, I call the quit() method. But again, same as you I got a problem when I make more than one test case.

I wonder if you already solved this problem and perhaps care to share it with me (and possibly others) :)

PS: I also would like to know how you call your Looper.myLooper().quit().

Thanks!

于 2010-12-09T09:33:24.210 に答える