CursorLoaderのテスト

現在開発中のアプリではCursorLoaderとSimpleCursorAdapterを使ってDBのデータをListViewに表示しています。
CursorLoader、SimpleCursorAdapterの使い方は以下の記事が詳しいです。

コジオニルク - Android - パワフルなCursorLoader

このCursorLoaderをテスト実行時にテスト用のDBからデータをロードする方法を紹介します。
下記のサンプルコードはScalaです。

まずはテスト用のContentResolverとContextを作成します。

TestContentResolver、TestContext

class TestContentResolver(context: Context) extends MockContentResolver {
private val TEST_PREFIX = "test_"
val provider = new UnitsProvider
val info = new ProviderInfo()
info.authority = UnitsProvider.AUTHORITY
info.enabled = true
info.packageName = UnitsProvider.getClass.getPackage.getName

val delegatingContext = new RenamingDelegatingContext(context, TEST_PREFIX)
provider.attachInfo(delegatingContext, info)
addProvider(UnitsProvider.AUTHORITY, provider)
}

class TestContext(context: Context) extends ContextWrapper(context) {
private lazy val resolver: ContentResolver = new TestContentResolver(context)

override def getContentResolver = resolver

// CursorLoader calls context.getApplicationContext inside it, so override this too.
override def getApplicationContext = this
}

MockContentResolverはモック作成用のクラスで、このクラスを継承して独自のContentResolverを作成します。
Contextの方も同様にMockContextというクラスもありますが、MockContextを継承して作成したモックをActivityUnitTestCaseのsetActivityContextで使用するとエラーとなってしまうため、ActivityUnitTestCaseではContextWrapperを継承したモックContextを使用します。
CursorLoaderは内部でgetApplicationContextを呼んで使用するため、こちらもオーバーライドしてモックを返すようにします。

UnitsProviderはCursorLoaderで使用するための私が作成した独自のContentProviderです。
通常ContentProviderではonCreateメソッドの中でContentProvider内部で使用するためのSQLiteOpenHelperを継承したクラスを初期化します。

UnitsProvider(独自のContentProvider) 抜粋

class UnitsProvider extends ContentProvider {
...

def onCreate(): Boolean = {
unitsDBHelper = new UnitsDBHelper(getContext)
true
}

UnitsDBHelper(抜粋)

  class UnitsDBHelper(context: Context) extends SQLiteOpenHelper(context, UnitsDBHelper.DB_NAME, null, UnitsDBHelper.DB_VERSION) {
...

RenamingDelegatingContextはテスト用のDBを作るためのContextで、これをSQLiteOpenHelperのコンストラクタに渡すと実行時に第2引数で指定したPrefix付きのSQLite DBファイルが作成されます。
詳細は以下の記事が詳しいです。

u1aryzの備忘録とか: RenamingDelegatingContextを使ってみた

TestContentResolverはこのRenamingDelegatingContextを作成し、provider.attachInfo(delegatingContext, info)の箇所でUnitsProviderにこのコンテキストをセットします。UnitsProviderの中でgetContextをするとセットされたRenamingDelegatingContextが返されます。
addProvider(UnitsProvider.AUTHORITY, provider)でここで作成したテスト用のUnitsProviderを登録します。

MainActivityTest

以下がテストコードです。ActivityInstrumentationTestCase2がよく使われると思いますが、モックContextを設定できないためモックContextを使いたい場合はActivityUnitTestCaseを継承させます。
以下のページで違いが詳しく解説されています。

Activity Testing | Android Developers

SoloはRobotiumのコードです。

class MainActivityTest extends ActivityUnitTestCase(classOf[MainActivity]) {

import com.shinichy.convertBox.TR

var solo: Solo = null
var activity: MainActivity = null

override def setUp() {
val mockContext = new TestContext(getInstrumentation.getTargetContext)
setActivityContext(mockContext)
startActivity(new Intent(), null, null)
solo = new Solo(getInstrumentation, getActivity)
activity = getActivity
getInstrumentation.callActivityOnStart(activity)
}

override def tearDown() {
solo.finishOpenedActivities()
}

def testUnitList() {
val unitList = activity.findView(TR.unit_list)
solo.sleep(1000)
assertEquals(5, unitList.getCount)
}
}

まず上で定義したモックContextを作成し、setActivityContextを呼び出してセットします。
この後startActivityで作成したActivityの中でgetContextやgetContentResolverを呼び出すとモックのTestContextやTestContentResolverが使われるようになります。

startActivityで作成したActivityはonCreateが呼ばれた状態でonStartやonResumeは呼ばれていません(ActivityInstrumentationTestCase2ではonResumeまで呼ばれた状態)。CursorLoaderはonStartの状態でないとロードされない(onLoadFinishedが呼ばれない)ため、getInstrumentation.callActivityOnStart(activity)でonStartの状態にするとロードされます。ロードは非同期に実行されるため、solo.sleep(1000)で1秒待ってDBからの読み込みが完了してからListViewで作成された行数をチェックしています。

MainActivity(抜粋)

以下はテスト対象のActivityです。CursorLoader、SimpleCursorAdapterを使ってListViewにDBのデータを表示しています。

class MainActivity extends Activity with TypedActivity with LoaderManager.LoaderCallbacks[Cursor] {
override def onCreate(bundle: Bundle) {
super.onCreate(bundle)
setContentView(R.layout.main)

adapter = new SimpleCursorAdapter(
this,
R.layout.list_row,
null,
Array[String]("symbol"),
Array[Int](R.id.symbol_text),
0
)

val unitList = findView(TR.unit_list)
unitList.setAdapter(adapter)
getLoaderManager.initLoader(0, null, this)
}

def onCreateLoader(id: Int, args: Bundle): Loader[Cursor] = {
new CursorLoader(getApplicationContext, Contract.UNITS.contentUri, null, null, null, null)
}

def onLoadFinished(loader: Loader[Cursor], c: Cursor) {
adapter.swapCursor(c)
}

def onLoaderReset(p1: Loader[Cursor]) {
adapter.swapCursor(null)
}
}