AndroidでContentProviderのモックを使ったテストを行う
ContentProviderからデータを取得しているアプリのテストコードを書くときに、テストデータとして端末内のデータを使わずにモックのデータを使いたいということは多いと思います。
今回は端末内の画像を一覧表示するアプリケーションを例にして、このようなテストコードの書き方を説明します。
テスト対象のActivity
以下のようなActivityをテストすることを考えます。
package com.polysfactory.mocktest; import android.app.ListActivity; import android.database.Cursor; import android.os.Bundle; import android.provider.MediaStore.Images; import android.provider.MediaStore.Images.ImageColumns; import android.widget.ArrayAdapter; public class MyActivity extends ListActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Cursor cursor = getContentResolver().query(Images.Media.EXTERNAL_CONTENT_URI, new String[] { ImageColumns.TITLE }, null, null, null); if (cursor != null) { String[] names = new String[cursor.getCount()]; int i = 0; while (cursor.moveToNext()) { names[i++] = cursor.getString(0); } cursor.close(); setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_expandable_list_item_1, names)); } } }
見ての通り、Adnroid標準のContentProviderであるMediaStoreから画像コンテンツの情報を全て取得して、タイトルをListViewの表示させるだけのActivityです。
このActivityを起動すると以下のような画面が表示されます。
こいつのテストをするとなると、Listの1件目にxxxxという画像名称が表示されていることを確認したい、というケースが出てきます。
当然、端末ごとに保存されている画像は異なりますが、極力端末に依存させないよう、Activityからはダミーのデータにアクセスさせて、それが表示されているかをテストする必要があるわけです。
テストプロジェクトの概要
今回のテストでは、MediaStoreのダミーとなるContentProviderを自作します。そしてMediaStoreのContent URIを指定された場合にそのダミーを見にいくようなContentResolverとContextのモックを作成し、Activityにインジェクトします。
そうすることでActivity側のコードを変えることなく、端末のデータベースを参照せずに、モックとして用意したデータを使ってテストを行うことが出来るようになるのです。
超適当クラス図にするとこんな感じです。
以下それぞれのクラスについて説明していきます。
ContentProviderのモックを準備する
いきなり肝ですが、MediaStoreのダミーとして動作するContentProviderのモックを用意してあげます。これはContentProviderを継承したクラスを自作します。
ここでのポイントはqueryメソッドをオーバーラードするときに、データベースから取得したCursorを返さずに、MatrixCursorクラスを使って仮想のカーソルを返してあげるところです。
public class MockImagesProvider extends ContentProvider { .... @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { MatrixCursor result = new MatrixCursor(projection); String[] values = new String[projection.length]; for (int i = 0; i < projection.length; i++) { ... values[i] = "hogehoge"; ... } result.addRow(values); return result; } ... }
これによって、テストデータをデータベースから読まこませず、ファイルから読み込んだ値などを返すことができます。
今回はサンプルのため、テストデータをソース中に書き込みます。以下、このクラスの全体ソースです。
public class MockImagesProvider extends ContentProvider { private static final String[] IMAGE_COLUMNS = { ImageColumns._ID, ImageColumns._COUNT, ImageColumns.DATA, ImageColumns.DATE_ADDED, ImageColumns.DATE_MODIFIED, ImageColumns.DISPLAY_NAME, ImageColumns.SIZE, ImageColumns.TITLE }; private static final String[] VALUE1 = { "1", "1", "/sdcard/hoge.jpg", "1000", "1000", "display_name_test", "100", "title_test" }; public static final int IMAGE = 1; public static final int IMAGE_ID = 2; @Override public boolean onCreate() { return true; } @Override public Uri insert(Uri uri, ContentValues values) { return null; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { MatrixCursor result = new MatrixCursor(projection); String[] values = new String[projection.length]; for (int i = 0; i < projection.length; i++) { for (int j = 0; j < IMAGE_COLUMNS.length; j++) { if (IMAGE_COLUMNS[j].equals(projection[i])) { values[i] = VALUE1[j]; } } } result.addRow(values); return result; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { return 0; } @Override public String getType(Uri uri) { return null; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; } }
CotentResolverのモックを作成する
第2の肝です。MediaStoreから画像の情報を取得するときのURIはcontent://media/external/imagesというURIになります。
ContentResolverのモックを作成して、content://mediaのURIに対しては、先ほど作成したMockImagesProviderにルーティングするように実装します。
public class MyMockContentResolver extends MockContentResolver { public MyMockContentResolver(Context context) { ContentProvider provider = new MockImagesProvider(); ProviderInfo providerInfo = new ProviderInfo(); providerInfo.authority = "media"; providerInfo.enabled = true; providerInfo.isSyncable = false; providerInfo.packageName = MockImagesProvider.class.getPackage().getName(); provider.attachInfo(context, providerInfo); super.addProvider("media", provider); } }
Contextのモックを作成する
ActivityにセットするためのContextのモックを作成します。
ContextWrapperを継承して、getContentResolver()だけをオーバーライドしてあげることで、ContentResolverを取得する部分だけをモックの動作に置き換えることができます。
public class MyMockContext extends ContextWrapper { private ContentResolver contentResolver; public MyMockContext(Context context) { super(context); contentResolver = new MyMockContentResolver(context); } @Override public ContentResolver getContentResolver() { return contentResolver; } }
テストケース
最後にテストを実行する部分のコードです。ActivityのテストはActivityInstrumentationTestCase2が使われることが多いですが、ActivityInstrumentationTestCase2はテスト対象のActivityに対するContextのインジェクションができないという制約があるため、代わりにActivityUnitTestCaseを使います。
ActivityUnitTestCase.setActivityContextメソッドを使うことで、先ほど作ったMockContextをActivityにセットすることが出来ます。
public class MockTestSample extends ActivityUnitTestCase<MyActivity> { public MockTestSample() { super(MyActivity.class); } @Smoke public void testSample() { Context mockContext = new MyMockContext(getInstrumentation().getTargetContext()); setActivityContext(mockContext); startActivity(new Intent(), null, null); final MyActivity activity = getActivity(); ListView listView = activity.getListView(); int count = listView.getCount(); String item0 = (String)listView.getAdapter().getItem(0); assertNotNull(activity); //表示されているデータが1件であることを確認する assertEquals(1, count); //表示されている画像名がtitle_testであることを確認する assertEquals("title_test", item0); } }
まとめ
ContentResolver、ContentResolver、Contextという長い道のりを経て、モックのデータでActivityのテストを行う方法について書きました。
これで何が嬉しいかといういうと、端末ごとのデータに依存せずにテストを実行できることです。一度テストデータとコードを書いてしまえば、どんな機種でも実行できるようにようになるので、品質を上げるにに必ず役立つと思ってます。