概要
startActivityForResult
とonActivityResult
が非推奨になったので、ActivityResultAPI
を使って書き直しました。
で、公式にFragment
向けのActivityResultRegistry
を置き換えてテストできる方法について触れている箇所があったのですが、
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で動かせるように重複して登録している物が多いため長くなっています。
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にする
class FooActivity : ScopedActivity() {
2. Koinのモジュールを作る
Koinの説明は割愛しますので、詳しくは調べてください。
(1)モジュールを作る
val scopeModules = module {
scope<FooActivity> {
scoped { get<AppCompatActivity>().activityResultRegistry }
}
}
// モジュール群
val appModules = listOf(
scopeModules
)
(2)アプリケーションクラスでKoinを設定する
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
if (BuildConfig.DEBUG) androidLogger(Level.DEBUG) else EmptyLogger()
androidContext(this@MyApp)
modules(appModules)
}
}
}
マニフェストファイルにアプリケーションクラスを設定するのもお忘れなく。
<application
android:name=".MyApp"
...>
(3)registerForActivityResultの第2引数を変更する
registerForActivityResult
の第2引数をget()
に変更します。
private val activityResultLauncher =
registerForActivityResult(
ActivityResultContracts.StartActivityForResult(), get()) {
onBarActivityResult(it)
}
これでKoinが同じクラスを返すモジュールを探してInjectionしてくれます。
3. テスト用のモジュール作成
(1)テスト用のActivityResultRegistryを定義
class TestResultRegistry() :
ActivityResultRegistry() {
override fun <I, O> onLaunch(
requestCode: Int,
contract: ActivityResultContract<I, O>,
input: I,
options: ActivityOptionsCompat?
) {
when (/* 条件 */) {
// caseに応じたdispatchResult処理
dispatchResult(requestCode, /*リザルトコード*/, /*リザルトデータ*/)
}
}
}
期待する結果を入れておく変数や関数などは適宜作成してください。
(2)差し替えモジュール作成
テストの際に差し替えるモジュールを作成します。
val testMockModule = module {
scope<FooActivity> {
scoped(override = true) { TestResultRegistry() as ActivityResultRegistry }
}
}
4. テストクラス
(1)テストクラスを作成
androidTestで作っていきます。
@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)テストごとに追加モジュールを使う
val testMockModule = module {
//scope<FooActivity> {
// scoped(override = true) { TestResultRegistry() as ActivityResultRegistry }
// }
}
@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関係の所は同じで動くはずです)
参考サイトなど