ActivityResultAPIのテストを書く

# 概要
`startActivityForResult`と`onActivityResult`が非推奨になったので、`ActivityResultAPI`を使って書き直しました。
で、公式に`Fragment`向けの`ActivityResultRegistry`を置き換えてテストできる方法について触れている箇所があったのですが、

https://developer.android.com/training/basics/intents/result?hl=ja#test

`Activity`でテストする場合のことが書いてなかったので調べて対応しました。



# 環境

|ツールなど|バージョンなど|
|:--|:--|
| MacbookPro |macOS Catalina 10.15.7|
|Android Studio |4.1.2|
|Java(JDK) | openjdk version "11.0.10" |
| Koin |2.2.2|
| activity-ktx | 1.2.1|

# 対応方法

**Koin**というDIライブラリを使って実現しました。
同じようなDIのライブラリを使ったり、あるいは**Mockito**で頑張れば似たようなことが出来るかと思います。

## 0. ライブラリなど
Koinとテスト関連のものを抜粋しています。
同じコードでInstrumentationテストとRobolectricで動かせるように重複して登録している物が多いため長くなっています。

```app/build.gradle

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    implementation 'com.google.android.material:material:1.3.0'
    implementation 'androidx.fragment:fragment-ktx:1.3.1'
    implementation 'androidx.activity:activity-ktx:1.2.1'

    testImplementation 'junit:junit:4.13.2'
    testImplementation 'org.assertj:assertj-core:3.19.0'
    testImplementation 'androidx.test.ext:junit:1.1.2'
    testImplementation 'androidx.test:runner:1.3.0'
    testImplementation 'androidx.test:rules:1.3.0'
    testImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    testImplementation 'androidx.test.espresso:espresso-contrib:3.3.0'
    testImplementation 'androidx.test.espresso:espresso-intents:3.3.0'

    // robolectric
    testImplementation 'org.robolectric:robolectric:4.5.1'

    androidTestImplementation 'androidx.test:runner:1.3.0'
    androidTestImplementation 'androidx.test:rules:1.3.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.3.0'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'org.assertj:assertj-core:3.19.0'
    androidTestUtil 'androidx.test:orchestrator:1.3.0'

    // Koin for Kotlin apps
    def koin_version = "2.2.2"
    // Testing
    testImplementation "org.koin:koin-test:$koin_version"
    androidTestImplementation "org.koin:koin-test:$koin_version"

    // Koin for Android
    implementation "org.koin:koin-android:$koin_version"
    // Koin AndroidX Scope feature
    implementation "org.koin:koin-androidx-scope:$koin_version"
    // Koin AndroidX ViewModel feature
    implementation "org.koin:koin-androidx-viewmodel:$koin_version"
```

## 1. 対象のActivityをScopedActivityにする

```FooActivity.kt

class FooActivity : ScopedActivity() {

```

## 2. Koinのモジュールを作る

Koinの説明は割愛しますので、詳しくは調べてください。

### (1)モジュールを作る

```modules.kt
val scopeModules = module {
    scope<FooActivity> {
        scoped { get<AppCompatActivity>().activityResultRegistry }
    }
}
// モジュール群
val appModules = listOf(
    scopeModules
)
```

### (2)アプリケーションクラスでKoinを設定する

```MyApp.kt
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()

        startKoin {
            if (BuildConfig.DEBUG) androidLogger(Level.DEBUG) else EmptyLogger()
            androidContext(this@MyApp)
            modules(appModules)
        }
    }
}
```

マニフェストファイルにアプリケーションクラスを設定するのもお忘れなく。

```AndroidManifest.xml
   <application
            android:name=".MyApp"
            ...>
```

### (3)registerForActivityResultの第2引数を変更する

`registerForActivityResult`の第2引数を`get()`に変更します。

```FooActivity.kt
    private val activityResultLauncher =
        registerForActivityResult(
          ActivityResultContracts.StartActivityForResult(), get()) {
            onBarActivityResult(it)
        }
```

これでKoinが同じクラスを返すモジュールを探してInjectionしてくれます。

## 3. テスト用のモジュール作成

### (1)テスト用のActivityResultRegistryを定義

```TestModules.kt
    class TestResultRegistry() :
        ActivityResultRegistry() {
        override fun <I, O> onLaunch(
            requestCode: Int,
            contract: ActivityResultContract<I, O>,
            input: I,
            options: ActivityOptionsCompat?
        ) {
            when (/* 条件 */) {
                // caseに応じたdispatchResult処理
                dispatchResult(requestCode, /*リザルトコード*/, /*リザルトデータ*/)
            }
        }
    }
```

期待する結果を入れておく変数や関数などは適宜作成してください。

### (2)差し替えモジュール作成

テストの際に差し替えるモジュールを作成します。

```TestModules.kt
val testMockModule = module {
    scope<FooActivity> {
        scoped(override = true) { TestResultRegistry() as ActivityResultRegistry }
    }
}
```

## 4. テストクラス

### (1)テストクラスを作成

androidTestで作っていきます。

```FooActivityTest.kt
@RunWith(AndroidJUnit4::class)
class FooActivityTest : AutoCloseKoinTest() {

    @get:Rule
    val rule = InstantTaskExecutorRule()

    @Before
    fun setUp() {
        loadKoinModules(testMockModule)
    }
    @Test
    fun bar() {
        ActivityScenario.launch(FooActivity::class.java).use { scenario ->
            scenario.onActivity {
            }

            // 別のActivityを起動させる
            onView(withId(R.id.barActivityLaunchButton)).perform(click())

            // resultがすぐにディスパッチされているので、結果を受け取って表示されているべき内容をチェックする
            onView(withText(getString(R.string.barbar)))
                .check(matches(isDisplayed()))
        }
    }
}
```

注意としては、この方法だと、直ぐに結果がdispatchされるため実際には`Activity`は何も起動していない点です。なので`ActivityMonitor`を使って起動した`Activity`のチェックを行うことは出来ません。

そこで、次のように必要なテストに応じて追加の差替えモジュールをkoinに読ませるようにすると良いかも知れません。

その場合、`testMockModule`から`TestResultRegistry`は除外しておく必要があります。

### (2)テストごとに追加モジュールを使う

```TestModules.kt
val testMockModule = module {
//scope<FooActivity> {
//        scoped(override = true) { TestResultRegistry() as ActivityResultRegistry }
//    }
}
```


```FooActivityTest.kt

    @Test
    fun bar_isLaunched() {
	    // ResultActivityの起動を監視
        val monitor = Instrumentation.ActivityMonitor(
          BarActivity::class.java.canonicalName, null, false
        )
        InstrumentationRegistry.getInstrumentation().addMonitor(monitor)
        onView(withId(R.id.barActivityLaunchButton)).perform(click())
        
        // ResultActivityが起動したか確認
        InstrumentationRegistry.getInstrumentation().waitForMonitorWithTimeout(monitor, 1000L)
        assertThat(monitor.hits).isEqualTo(1)
    }

    @Test
    fun bar_resultCheck() {
        val testRegistry = TestRegistry()
        // 追加のモジュール
        val scopedModule = module {
            scope<FooActivity> {
                scoped(override = true) { testRegistry as ActivityResultRegistry }
            }
        }
        loadKoinModules(scopedModule)

        ActivityScenario.launch(FooActivity::class.java).use { scenario ->
            scenario.onActivity {
            }

            // 別のActivityを起動させる
            onView(withId(R.id.barActivityLaunchButton)).perform(click())

            // resultがすぐにディスパッチされているので、結果を受け取って表示されているべき内容をチェックする
            onView(withText(getString(R.string.barbar)))
                .check(matches(isDisplayed()))
        }
    }
```

## 5.Roblectric版

Robolectricでも同じコードで動くのを確認しています。
ただし、Roblectricで動かしている場合はEspressoが`onView`でダイアログ上のViewを拾えないので、ダイアログを表示している場合にはテストコードを100%流用することが出来ません。ご注意下さい。
(ActivityResultAPI関係の所は同じで動くはずです)

# 参考サイトなど
https://www.fixes.pub/program/314804.html