Robolectric + JetpackでActivityのonActivityResultメソッドをテストする

Robolectric4.xからユニットテスト環境で、android testと(ほぼ?)同じテストコードを動かすことが可能になりました。 まだ、完全に互換性があるとはいえませんが、Espressoライブラリが動く、AndroidJUnit4ランナーが使えるなど、かなりの部分が共通化出来ます。

この記事では、ユニットテストでActivity.onActivityResultのテストをどこまでandroid testのように書けるかを検証します。

テスト対象コード

まず最初に、テスト対象コードは次のようになっています。

class MainActivity : AppCompatActivity() {
  ...
  override fun onCreate(savedInstanceState: Bundle?) {
    ...

    button.setOnClickListener {
      startActivityForResult(
        Intent(this, Sub2Activity::class.java).apply {
          putExtra("fuga", "hoge")
        },
        1
      )
    }
  }

  override fun onActivityResult(
    requestCode: Int,
    resultCode: Int,
    data: Intent?
  ) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == 1) {
      if (resultCode == Activity.RESULT_OK) {
        val value = data!!.getIntExtra("test", -1)
        button.text = value.toString()
      }
    }
  }
}
class Sub2Activity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.sub_act)

    button.setOnClickListener {
      val intent = Intent()
      intent.putExtra("test", 10)
      setResult(Activity.RESULT_OK, intent)
      finish()
    }
  }
}

これはMainActivityでstartActivityForResultがコールされ、Sub2ActivityでsetResultで値をセットし、MainActivityのonActivityResultで結果を受け取るサンプルコードになります。

では、テストを書いていきます。

テストコード

以下が、今回書いたテストコードになります。

@RunWith(AndroidJUnit4::class)
internal class MainActivityTest {
  @get:Rule val intentsTestRule = IntentsTestRule(MainActivity::class.java)

  @Test
  fun onActivityResultTest() {
    val expectCode = 10

    // assertion setResult
    val scenario = ActivityScenario.launch(Sub2Activity::class.java)
    scenario.onActivity {
      it.findViewById<View>(R.id.button).performClick()
    }

    // assertion resultCode
    val result = scenario.result
    assertThat(result.resultCode).isEqualTo(Activity.RESULT_OK)

    // assertion intent params
    val bundleSubject = IntentSubject.assertThat(result.resultData).extras()
    bundleSubject.integer("test").isEqualTo(expectCode)

    scenario.close()

    Intents
      .intending(
        IntentMatchers.hasComponent(
          ComponentName(
            ApplicationProvider.getApplicationContext<Application>(),
            Sub2Activity::class.java
          )
        )
      )
      .respondWith(result)

    val main = ActivityScenario.launch(MainActivity::class.java)
    main.onActivity {
      it.findViewById<View>(R.id.button).performClick()

      // assertion intent for startActivity(ForResult)
      val name = ComponentName(
        ApplicationProvider.getApplicationContext<Application>(),
        Sub2Activity::class.java
      )
      Intents.intended(IntentMatchers.hasComponent(name))
      Intents.intended(IntentMatchers.hasExtra("fuga", "hoge"))

      // assertion onActivityResult behaves
      Espresso
        .onView(ViewMatchers.withId(R.id.button))
        .check(ViewAssertions.matches(ViewMatchers.withText(expectCode.toString())))
    }
  }
}

上から順番に重要な部分を説明していきます。

@get:Rule val intentsTestRule = IntentsTestRule(MainActivity::class.java)

これは、Espresso-Intentsを使うときに必要なルールです。Intens.intendedintendingを使うために必要なルールになります。

val scenario = ActivityScenario.launch(Sub2Activity::class.java)
scenario.onActivity {
    it.findViewById<View>(R.id.button).performClick()
}

ActivityScenarioはActivityを起動するためのクラスです。これはSub2Activityを起動して、ボタンをクリックするという意味になります。 ボタンがクリックされると、Sub2ActivityでsetResultが発火するようになっています。

val result = scenario.result
assertThat(result.resultCode).isEqualTo(Activity.RESULT_OK)

val bundleSubject = IntentSubject.assertThat(result.resultData).extras()
bundleSubject.integer("test").isEqualTo(expectCode)

ActivityScenarioでは、ActivityResultクラスから結果を取得することが出来ます。このクラスにはresultCodeと、resultDataがセットされており、それらの値をTruthを使いチェックします。この場合、setResultで、resultcodeにActivity.RESULT_OKが、resultdataにはキー名test、値10がセットされていることを確認してします。

ここまでで、Sub2ActivityのsetResultで正しい値をセットしていることがテスト出来ます。

では次に、MainActivityで上記の値を受け取れることをテストしていきます。

Intents
  .intending(
    IntentMatchers.hasComponent(
      ComponentName(
        ApplicationProvider.getApplicationContext<Application>(),
        Sub2Activity::class.java
      )
    )
  )
  .respondWith(result)

Intents.intendingはマッチしたIntentが発行されたときに、onActivityResultに結果を返すAPIになります。 MainActivityのonActivityResultに、先ほどのSub2Activityの結果を渡すという意味になります。

val main = ActivityScenario.launch(MainActivity::class.java)
main.onActivity {
  it.findViewById<View>(R.id.button).performClick()

  val name = ComponentName(
    ApplicationProvider.getApplicationContext<Application>(),
    Sub2Activity::class.java
  )
  Intents.intended(IntentMatchers.hasComponent(name))
  Intents.intended(IntentMatchers.hasExtra("fuga", "hoge"))

  Espresso
    .onView(ViewMatchers.withId(R.id.button))
    .check(ViewAssertions.matches(ViewMatchers.withText(expectCode.toString())))
}

まずは、クリックイベントを発火し、startActivityForResultをコールします。渡したIntentをIntents.intendedで正しいことを確認します。最後に、Espressoを使って、onActivityResultの結果を正しく反映されているかを確認します。

これで、テスト完了です😃 2つのActivityに関連するonActivityResultのテストが無事に出来ました!!

補足

Espressoにはご存知、clickをするためのAPIがあるのですが、うまく動きませんでした。

// not working!!
Espresso
  .onView(ViewMatchers.withId(R.id.button))
  .perform(ViewActions.click())

調べたんですが、原因がわかりませんでした😂分かり次第追記します。

まとめ

今回の検証に用いたサンプルコードはsatoshun-android-example/Testsにあります。

もっと良い書き方を知っているよと言う人は教えて頂けるととても嬉しいです😃😃😃

Written by
あんどろいどでぃべろっぱぁー🍎