【Android Studio】UnitTestでHandler処理のテスト【日本語】

Android Studio(JAVA)のJUnitで、Mockを使ったテストをしてみた。

そこで、UnitTestだとAndroid.osに頼った処理はモック化しなければいけないっぽい?ことを知った。

その弊害で、Handlerを使ってバックグラウンドの処理のテストをする方法に悩み、無事解決したので備忘録として掲載しておく

 

他のプログラミングに関する記事はこちら

スポンサーリンク


 

【前提】

robolectricというライブラリを使っても出来るらしい。

が、PowerMockitoとの同時利用が何やら面倒そう。

なので、今回はrobolectricを使わずに、Handlerにpostした処理のテストが出来ないか模索した。

 

【結論】

Handlerクラスをモック化し、postメソッド(もしpostDelayedを使っているならpostDelayedメソッド)をthenAnswerを使ってスタブ化すればイケる。

thenAnswerの使い方は過去の記事(こちら)にまとめているので、見て頂ければ。

 

【実装方法の例】

テスト対象とする「Sample」クラスは以下の通り。

今回は、SubSampleのmethod(Runnable)に対し、Sampleクラス内で定義したtask()メソッドを実行するRunnableを渡し、動くかどうか試す。

動いたかどうかは、publicであるtestNumが0から3になったかどうかで確認する。

テスト対象のSampleクラス

public class Sample {

    public int testNum = 0;

    public void backgroundTaskRun(){
        // SubSample生成
        SubSample subSample = new SubSample();

        // SubSampleのメソッド実行
        subSample.method(new Runnable() {
            @Override
            public void run() {
                task();
            }
        });
    }

    private void task(){
        int i = 1;
        int n = 2;

        testNum = i + n;
    }
}

 

「SubSample」クラスは以下の通り。

テスト対象である「Sample」クラスから呼ばれるのはmethod(Runnable)メソッドで、引数であるRunnableをpostで実行する内容となっている。

SubSampleクラス

public class SubSample {
    public void method(Runnable backgroundTask){
        HandlerThread workerThread = new HandlerThread("test");
        workerThread.start();
        Handler workerHandler = new Handler(workerThread.getLooper(), workerHandlerCallBack);

        workerHandler.post(backgroundTask);
    }

    private Handler.Callback workerHandlerCallBack = new Handler.Callback() {
        @Override
        public boolean handleMessage(@NonNull Message msg) {
            return false;
        }
    };
}

 

で、最後にテストコード「SampleTest」クラスが以下の通り。

詳細は後述するが、とりあえず「Sample」「SubSample」「SampleTest」をコピペすれば動きはする……と思う。

テストコードのSampleTestクラス

@RunWith(PowerMockRunner.class)
@PrepareForTest({SubSample.class})
public class SampleTest {

    // テスト対象
    Sample mSample;

    // 初期処理
    @Before
    public void setUp() {
        this.mSample = new Sample();
    }

    @Test
    public void test1() throws Exception{
        // ハンドラをモック化
        Handler mockHandler = Mockito.mock(Handler.class);

        // スタブ作成
        Mockito.when(mockHandler.post(Mockito.any(Runnable.class))).thenAnswer(new Answer(){
            @Override
            public Object answer(InvocationOnMock invocation) throws Exception{
                // getArgumentを使った場合
                Runnable runnable = invocation.getArgument(0, Runnable.class);
                runnable.run();
                return null;
            }
        });

        // モックオブジェクトを注入
        PowerMockito.whenNew(Handler.class).withArguments(Mockito.any(), Mockito.any(Handler.Callback.class)).thenReturn(mockHandler);

        // 実行
        this.mSample.backgroundTaskRun();

        // 処理を待つ……
        Thread.sleep(5000);

        // 評価
        assertEquals(3,this.mSample.testNum);
    }
}

 

 

【詳細】

長くなって見にくくなるかもしれないが、ズブの素人である自分が後から見返しても理解できるように細かめに記載する。

忘れてしまってこの記事を見返している将来の自分へ → 「めげずに頑張って読め!笑」

 

 

前提として、SampleクラスとSubSampleクラスでどんな処理になっているかは理解しておくこと。

説明用に最低限の記載にしているので、時間をかければ読みとけないことはないはず。

以降の説明は、SampleクラスのbackgroundTaskRun()メソッドを実行して、最終的にSampleクラスのtestNumの値が0から3に更新される流れを把握しているものとして説明する。

※テストコードである「SampleTest」の詳細のみ記載

 

 

以下の記述は、PowerMockを使うために必要な定型文。

@PrepareForTestには、モックを適用させるクラスを記載する。

@RunWith(PowerMockRunner.class)
@PrepareForTest({SubSample.class})

注意点として、SubSampleクラスがSampleクラスの継承先として指定している場合は、@PrepareForTestの中身は「Sample.class」になる。

……というか、多分Handlerを使うようなアプリは、継承して使うような使い方の方が多いと思う。

とにもかくにも、「モック化したクラスを使いたいクラスを@PrepareForTestに記載する」と覚えておけば間違いないと思う。

 

 

以下の記述は、Handlerクラスをモック化している。

冒頭でも述べた通り、AndroidのUnitTestではAndroid.osのクラスは全てモック化する必要があるっぽい。

        // ハンドラをモック化
        Handler mockHandler = Mockito.mock(Handler.class);

 

 

以下の記述は、モック化したHandlerクラスのpostメソッドの処理内容を設定している。

thenAnswerを使っているが、thenAnswerの使い方については過去の記事(こちら)にわかりやすく、まとめ済。

HandlerクラスにRunnable(バックグラウンドで処理させたい内容)がpostされた場合に、渡されたRunnableの内容をrun()で実行させる!という処理内容に置き換えている。

UnitTestでAndroid.osの機能が使えれば、こんな面倒なことしなくてよさげなんだけど……仕方ない。

もしpostではなくpostDelayedを使っている場合、whenの中身をpostDelayed用に書き換えればOK。

when(mockHandler.postDelayed(Mockito.any(Runnable.class), Mockito.any(Long.class))) みたいな?(postDelayedの第2引数ってLong型だったっけ?)

        // スタブ作成
        Mockito.when(mockHandler.post(Mockito.any(Runnable.class))).thenAnswer(new Answer(){
            @Override
            public Object answer(InvocationOnMock invocation) throws Exception{
                // getArgumentを使った場合
                Runnable runnable = invocation.getArgument(0, Runnable.class);
                runnable.run();
                return null;
            }
        });

 

 

以下の記述で作成したHandler型のモックを注入している。

以下はPowerMock用の注入なので、もしMockitoの注入で済む場合はそれで対応。

とにかく、モック化したものを注入する、ということを忘れないようにする。

        // モックオブジェクトを注入
        PowerMockito.whenNew(Handler.class).withArguments(Mockito.any(), Mockito.any(Handler.Callback.class)).thenReturn(mockHandler);

注意点として、今回はwithArgumentsの第1引数は「Mockito.any(HandlerThread.class)」ではなく、「Mockito.any()」としている。

理由は、SubSampleテストでHandlerクラスを生成する場合に、第1引数にHandlerThread型のオブジェクトをいれているが、このHandlerThread型もAndroid.osの機能のためnullとなってしまうことから。

もしHandlertThread型もモック化して注入してたり、そもそもHandler型のオブジェクトを生成する場面で引数が異なる場合は、それに応じてwithArgumentsの中身を変えてあげる必要がある。

 

 

色々やった結果を元に、以下記述で処理を実行している。

処理を待つ……という処理はなくてもいいかもしれない。

バックグラウンドで処理させている部分のテストをする場合、バックグラウンドで処理させている内容が終わる前にテストコードの処理が終わると正常にカバレッジが取れないため、待機時間を設けている。

今回はスタブの中でrunしているだけなので、なんかいらない気がするけど、もうこの件について調査するのが疲れてきたので、念のためいれてる。

        // 実行
        this.mSample.backgroundTaskRun();

        // 処理を待つ……
        Thread.sleep(5000);

 

 

これでThreadにpostした内容もUnitTestでrobolectricを使わずにテストが出来る。

以下図はちゃんとカバレッジで処理が通った確認をした画像。

 

 

この問題を解決するのに数日かかった……自分のスキルのなさが悔やまれる。

あと、言い訳だが、詳細に書いてある日本語の記事が少ないのも大きな理由だと感じた。

自分のように迷えた人の助けに、本記事がなってくれたら嬉しいと思う。

 

 

他のプログラミングに関する記事はこちら

スポンサーリンク