スキップしてメイン コンテンツに移動

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

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
同一のレシーバーを登録すると、複数のテストを実行するときにリークする?

コメント

このブログの人気の投稿

Javaでprivateなfieldやmethodにアクセスする

JUnitでテストしてると、privateなフィールドにアクセスして、値を参照したりセットしたりしたくなるわけですが、よく使うのでメモしておきます。 例えば、次のような対象のクラスがあるとします。 public class ParentClass { private String hoge = "ParentClass!!"; public void dispMsg() { System.out.println("dispMsg:" + hoge); } private void privateDispMsg(String msg) { System.out.println("dispMsg:" + msg); } }

GolangでWindows GUIアプリケーション

GUIアプリ作成の前提 社内ツールとしてexeで配布 開発環境はGoLandを使う 社内ツールとしてexeを配布ということであれば、Visual StudioでC#による開発だと思います。しかしながら、Go言語を習得したいのと、GoLandの補完機能が便利で、Android Studio使っていたこともあり、とっつきやすいという点からGo縛りでGUIアプリケーションを考えたいと思います。 lxn/walk Windows application library kit for Go. Windows向けしか考えていないので、まずこのライブラリなのですが、ボタンやコンボボックスが思ったように並ばなかったり、手軽にイメージボタンを配置したりなど、簡単にレイアウトを変更できない課題に直面しました。レイアウト作成だけで時間を取られてしまいます。そう言えば、Androidアプリ開発のときは、XMLでデザイン部分を切り離してたのを思い出して、今回の調査の運びとなりました。 fyne-io/fyne Cross platform native GUIs designed for Go based on Material Design. Supports: Linux, macOS, Windows, BSD, iOS and Android. walkと比べると、クロスプラットフォームで作成できるのですが、こちらもコードの中にデザインを書いていく形でした。 therecipe/qt therecipe/qt allows you to write Qt applications entirely in Go or JavaScript. Qt Creatorなるものがあるようで、デザイン部分を切り離せる印象はあるものの、最新のPCでもビルドに時間がかかるようで、学習コストもかかるという記事を見かけて断念しました。 どうしたものかとツイートしましたら、下記のようにご助言をいただいた。 同じ課題に対し色々試した結果、PWAのフルキャッシュドに落ち着いた。中身はGo+WASMで。 https://t.co/e60whDTV16 — のぼのぼ📡 (@nobonobo) July 21, 2020 PWA 早速調べてみました。Googleが進めているプロジェクトで、ネイティブアプリのよ

Eclipseの高速化メモ

Eclipseが重いと一言に言っても、いろいろな工程での話があると思いますが、過去記事のなども含めてこの記事にピックアップしておきたいと思います。以下はWindows環境での話です。