2012-05-26

[Android]モックを使ってContentProviderを使ったメソッドのテストを行う

表題の内容を行います。

Activityなどのテストは意外と簡単にできますが、ContentProviderの結果を使ったテストだと意外に難しい。
ユニットテストは書かずに実機を使って、メソッドを実行するように手で操作という形で直接やってしまうと思います。


テストデータにContentProviderを使い実機からデータを取得するのではなく
テストデータとして端末内のデータを使わずにモックのデータを使いたいという感じです。

というわけでやってみます。

お題は以下のコード

「端末の発着信履歴すべてを取得して文字列として返す」というメソッド
多分、こんな感じで書かれているのではないだろうかと思います。
public static String getContactData(Context context) {
    String data = "";
    
    if(context == null){
        return data;
    }

    ContentResolver resolver = context.getContentResolver();
    if(resolver == null){
        return data;
    }
        
    Cursor cursor = resolver.query(CallLog.Calls.CONTENT_URI,new String[]{CallLog.Calls.NUMBER} , null, null, null);
    
    if(cursor == null){
        return data;
    }
    StringBuffer sb = new StringBuffer(data);
    cursor.moveToFirst();

    for(int i=0;i< cursor.getCount(); i++){
        sb.append(cursor.getString(cursor.getColumnIndex(CallLog.Calls.NUMBER)));
        sb.append("\n");
        cursor.moveToNext();
    }
        
    return sb.toString();
}


言い忘れました。
上記メソッドの何をテストするかということですが
「Cursorで取得した件数が0件(つまり発着信履歴が無い)場合、空文字を返すことを確認する」
というふうにします。

上記メソッドをテストする上で障害(必要)になりそうなものは以下の4つです
・Context
・ContentResolver
・ContentProvider
・Cursor

幸いなことにこれら4つはAPIにMockクラスが用意されています。
というわけなので、これらを全てモックに差し替えるようにします。
因にこれらは全て継承して使うようにしてください。UnSupportedExceptionが飛んでテストできなかったりw
ドキュメントにも継承して使うように記載されています。


では、本題のテストコードを作成します。

▲まずはCursorのモックを作成します
以下の動作をするように設定します
・Cursor#getCount()の戻り値を0にする
・直前にCursor#moveToFirst()が使用されているのでこれも実装する(戻り値は見ていないので何でもよい)
class Return0Cursor extends MockCursor{

    @Override
    public int getCount() {
        return 0;
    }

    @Override
    public boolean moveToFirst() {
        return true;
    }
}


▲次に作成したMockCursorを返すようなContentProviderを作成します
以下の動作をするように設定します
・ContentProvider#query(Uri, String[], String, String[], String)の戻り値を先ほど作成したモックを返すようにする
class Provider extends MockContentProvider{

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
        String[] selectionArgs, String sortOrder) {
        return new Return0Cursor();
    }
}


▲次に超重要なContentResolverの動作を差し替える
何故、重要かというとこいつがURIを見てどのContentResolverを返すか決定するからです。

コンストラクタの第一引数は「プロバイダに関連づける一意な値」を設定してください
つまり、「content://<ここの値>/hoge」です(ここの値と記載したもの)
電話の発着信履歴であれば「call_log」,ICCのデータであれば「icc」
画像であれば「media」を設定します(content://media/external/images/media)

以下の動作をするように設定します
・コンストラクタに先ほど作成した電話の発着信履歴の値と先ほど作成したモックを設定
class Reslover extends MockContentResolver{

    public Reslover() {
        Provider provider = new Provider();
        super.addProvider("call_log", provider);
    }
}


因にテスト対象のコードを見て以下のように設定する方もおられると思います。
super.addProvider(CallLog.Calls.CONTENT_URI.toString(), provider);

がこの方法は使用できません。なぜならcontent://call_log/callsになっているからです。



▲次に作成したMockContentResolverを返すようなContextを作成します
以下の動作をするように設定します
・Context#getContentResolver()の戻り値を先ほど作成したContentReolverを返すようにする
class ReturnMock extends MockContext{

    @Override
    public ContentResolver getContentResolver() {
        return new Reslover();
    }
}


▲最後にこれらを使用するテストコードを記載
ActivityUnitTestCaseかAndroidTestCaseを継承したテストクラスを用意してください。
アクティビティの起動に作成したモックを設定することができるからです。
以下を実行するとモックのデータを読み込んでテストが動いてくれます。
実行するとグリーンになる、、、はず。。。
public void testCase3()throws Exception{
    ReturnMock context = new ReturnMock();
    try{
        assertEquals("", Utils.getContactData(context));
    }catch (Exception e) {
        fail("テスト失敗");
    }
}


▲まとめ
いかがだったでしょうか?
以下をモックに差し替えることで、ContentProviderを使用したメソッドに対するテストが実行できるようになります。
・Context
・ContentResolver
・ContentProvider
・Cursor

とりあえず嬉しそうなことを書くと

・端末ごとのデータに依存せずにテストを実行できる
・どんな機種(エミュレータ含む)でも実行できるようにようになる

ぐらいじゃないだろうか。それ以外思いつかない。
しいて他にあげるとすると大量のデータを与えたらどうなるの?とかはテストできるんじゃないだろうか。

欠点はテストを実行するまでの道のりが長いことではないだろうか。。。
mockitoみたいなライブラリがあればいいんだが(AndroidMockで差し替えれるのかなぁ)

0 件のコメント:

コメントを投稿

Blogger Syntax Highliter