Android、Java、Web系、Linux、マラソン等の備忘録

2013/06/06

Handlerによる定期処理の挙動をテストするにあたり

0 件のコメント
androidのServiceにandroid.os.Handlerを継承したクラスを実装して、定期的なバックグラウンド処理を実装することを考える。この定期処理は、バックグラウンドの処理結果によって、次開始するまでのsleep時間を変更するような実装にしたい。例えば、連携するWebサービスのAPIを実行したら電波が悪くて接続できなかった場合、またあるときはそのWebサーバが一時的にダウンしていた場合、前者はリトライの間隔を短く、後者は間隔を長くしたい。そのような挙動をテストしたいのだけど、問題が2つある。
  1. Handlerを使った処理でsleepしている間にJUnit側の処理が進む(または終わってしまう)ので、sleepからの復帰を待つ(同期する)必要がある
  2. sleepからの復帰をJUnit側はどうやって知るか
1に関しては、java.util.concurrent.CountDownLatchを使うことで待たせることができる。とりあえず、非同期処理を待機するかどうかをHandlerではなく、AsyncTaskを継承したクラスの単体テストで、CountDownLatchの挙動を確認してみる。

例えばアプリ側に、以下のようなAsyncTaskを継承したクラスがあった場合、

import android.os.AsyncTask;
import android.util.Log;

public class DummyTask extends AsyncTask <Void, Void, Void>{

    @Override
    protected Void doInBackground(Void... params) {

        Log.v("DummyTask", "非同期開始");
        // 任意の非同期処理
        for(int i=0;i<20;i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        Log.v("DummyTask", "非同期終了");
        return null;
    }

    @Override
    protected void onPostExecute(Void result) {
        // 非同期処理後の処理
        Log.v("DummyTask", "非同期処理後");
    }
}

非同期処理の終りをJUnit側が知るには、onPostExecute()をJUnit側でOverrideする。そして、そこで待機解除のメソッドを実行する。以下JUnit側の処理、

    final CountDownLatch mSignal = new CountDownLatch(1);

    public void testCountDownLatch() throws Throwable {

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                try {
                    DummyTask task = new DummyTask() {
                        @Override
                        protected void onPostExecute(Void result) {
                            super.onPostExecute(result);
                            // 非同期処理後の処理をオーバーライド
                            Log.v("CountDownLatchSample", "DummyTask戻り!");
                            mSignal.countDown();
                        }
                    };
                    task.execute();
                } catch (Exception e) {
                    fail();
                }
            }
        });

        // ここで非同期処理が終わるのを待つ
        mSignal.await(10, TimeUnit.SECONDS);

        Log.v("CountDownLatchSample", "おわりー");
    }
runTestOnUiThread内で実行させてるのは、AsyncTaskを継承しているDummyTaskはUIスレッドで実行しないと正しく動作しないため。このプログラムでは、DummyTaskのdoInBackground()内を処理している間、JUnit側ではmSignal.await(10, TimeUnit.SECONDS);のところで待機する。この待機が解除されるのは、オーバーライドしているonPostExecute()の mSignal.countDown();の部分。CountDownLatchのインスタンス生成時に引数1を渡しているので、 mSignal.countDown()を1回呼べば待機が解除される。mSignal.await()の引数は、10秒以上待っても返ってこない場合は解除されるようにするもの。引数を設定しなければ、ひたすら待ち続けるので設定しておいた方がいいと思う。

このテストを実行すると、

非同期開始
非同期終了
非同期処理後
DummyTask戻り!
おわりー
というログが得られる。ちなみにmSignal.await()を設けず実行すると、非同期終了が表示される前にテストが終わってしまう。ここまで、問題の1つ目までは解決できたが、2つ目はどうするか...
2つ目は冒頭の実装例で挙げたようにサービスで動いている挙動をテストしたいので、先ほどの単体テストのようにはいかず困った。

どういうやり方がベストかわからないけど、とりあえず苦肉の策ということで解決を図る。Handlerを実装しているクラスは、sleepからの復帰時にメッセージを飛ばすよう実装している。そのメッセージをJUnit側でも拾って、メッセージを拾ったタイミングでmSignal.countDown()を実行するというもの。まあその実装有りきなんだけど....
ソースはJUnit側に以下のメッセージ受信役を追加する

    private static class DummyReceiver extends BroadcastReceiver {

        private CountDownLatch mSignal = null;

        public DummyReceiver(Activity activity, CountDownLatch signal) {
            mSignal = signal;
            // Handlerのsleep復帰でメッセージを受け取るクラスの名前
            String action = BackgroundReceiver.class.getSimpleName();

            IntentFilter filter = new IntentFilter(action);
            activity.registerReceiver(this, filter);
        }

        @Override
        public void onReceive(Context context, Intent intent) {
            // 待機解除を
            mSignal.countDown();
        }
    }

後はJUnitで該当のServiceを起動して、Handlerを実装部分を処理するよう促してやれば、JUnitのこのonReceiveにも飛んでくる。ちなみに、Serviceの話をしてたのにDummyReceiverにActivityを渡しているのは、ActivityからServiceを起動するようにしているため。

これであとは、エラーになるよう仕向けたり、待機している時間の長さを測るように肉付けしてやればバックグラウンド処理の挙動をテストできると思う。まだ試してない。

今回はIntentFilterのメッセージを拾うようにできたけど、メッセージを飛ばさない実装の場合は、どうするか。考えられるのは、Serviceをバインドして、何かの値をループで監視するか...うーん、もっと修行が必要ですね...

参考:まこちの覚え書き AsyncTaskをJUnitでテストする方法
参考:A developer's notes: Android Service test - callback method not firing


PR:JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)
PR:テスト駆動開発入門
PR:入門 Androidアプリケーションテスト

追記:2013/06/24
同一のレシーバーを登録すると、複数のテストを実行するときにリークする?

0 件のコメント :

コメントを投稿