2013年7月17日水曜日

[Android] 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)
  }
}

0 件のコメント:

コメントを投稿