AndroidのJUnitテストは超ムズい（３）

# AndroidのJUnitテストは超ムズい、EspressoでInstrument test
[AndroidのJUnitテストは超ムズい](https://qiita.com/cozyk100/items/550f39d5fbb8df0dbea0)
[AndroidのJUnitテストは超ムズい（２）](https://qiita.com/cozyk100/items/2e257d376bffb7117b10)
の続編です。

Robolectricでだいたいいい感じでJUnitテストができたので、RobolectricではできなかったテストをEspressoを使ってInstrument testで書いてみます。

# そもそもテストが起動しない
初っ端からエラーでテストが起動しません。(´・ω・`)
```
java.lang.NoClassDefFoundError: io.mockk.impl.JvmMockKGateway
at io.mockk.junit4.MockKRule.finished(MockKRule.kt:64)
at org.junit.rules.TestWatcher.finishedQuietly(TestWatcher.java:122)
at org.junit.rules.TestWatcher.access$400(TestWatcher.java:52)
at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:70)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)

（・・・中略・・・）

... 28 more
Caused by: io.mockk.proxy.MockKAgentException: MockK could not self-attach a jvmti agent to the current VM. This feature is required for inline mocking.
This error occured due to an I/O error during the creation of this agent: java.io.IOException: Unable to dlopen libmockkjvmtiagent.so: dlopen failed: library "libmockkjvmtiagent.so" not found
```
どうやらmockkが出している例外のようです。
> libmockkjvmtiagent.so: dlopen failed: library "libmockkjvmtiagent.so" not found

って言ってるけど、libmockkjvmtiagent.soってlinuxのshared libraryじや？今、Windows版で動かしているのに・・・
ネットで色々検索すると、[gitHubのissue](https://github.com/mockk/mockk/issues/297)にありました。
```gradle:build.gradle
android {
    ・・・
    testOptions {
        ・・・
        packagingOptions {
            jniLibs {
                useLegacyPackaging = true // espresso, mockk
            }
        }
    }
}
```
を追加するとこの事象は解消しました。何故かはわかりません。プロジェクトによって出る/出ないがあります。

これが、解消したらまた次。(´・ω・`)

```
java.lang.IncompatibleClassChangeError: Superclass kotlinx.coroutines.flow.StateFlow of kotlinx.coroutines.flow.StateFlow_1_Proxy is an interface (declaration of 'kotlinx.coroutines.flow.StateFlow_1_Proxy' appears in /data/user/0/jp.co.suzuken.materialrequest/app_dxmaker_cache/Generated_-767958641.jar)
at java.lang.VMClassLoader.findLoadedClass(Native Method)
at java.lang.ClassLoader.findLoadedClass(ClassLoader.java:738)
at java.lang.ClassLoader.loadClass(ClassLoader.java:363)
at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
at com.android.dx.stock.ProxyBuilder.loadClass(ProxyBuilder.java:358)
at com.android.dx.stock.ProxyBuilder.buildProxyClass(ProxyBuilder.java:340)
at io.mockk.proxy.android.transformation.AndroidSubclassInstrumentation.subclass(AndroidSubclassInstrumentation.kt:37)
at io.mockk.proxy.common.ProxyMaker.subclass(ProxyMaker.kt:144)
at io.mockk.proxy.common.ProxyMaker.proxy(ProxyMaker.kt:53)
at io.mockk.impl.instantiation.JvmMockFactory.newProxy(JvmMockFactory.kt:34)
at io.mockk.impl.instantiation.AbstractMockFactory.newProxy$default(AbstractMockFactory.kt:24)
at io.mockk.impl.instantiation.AbstractMockFactory.temporaryMock(AbstractMockFactory.kt:127)
at io.mockk.impl.recording.states.RecordingState$call$temporaryMock$1.invoke(RecordingState.kt:69)
at io.mockk.impl.instantiation.JvmAnyValueGenerator$anyValue$2.invoke(JvmAnyValueGenerator.kt:35)
at io.mockk.impl.instantiation.AnyValueGenerator.anyValue(AnyValueGenerator.kt:34)
at io.mockk.impl.instantiation.JvmAnyValueGenerator.anyValue(JvmAnyValueGenerator.kt:31)
at io.mockk.impl.recording.states.RecordingState.call(RecordingState.kt:75)
at io.mockk.impl.recording.CommonCallRecorder.call(CommonCallRecorder.kt:53)
at io.mockk.impl.stub.MockKStub.handleInvocation(MockKStub.kt:270)
at io.mockk.impl.instantiation.JvmMockFactoryHelper$mockHandler$1.invocation(JvmMockFactoryHelper.kt:24)
```
うーむ。これもmockk絡み。mockkでViewModelでStateFlowをmock化しているところで出ています。mockkとInstrument testの相性が悪いのかな？と思ったのですが、[mockkはちゃんとInstrument testをサポートしています。
](https://mockk.io/ANDROID.html)
（Android P未満とP以上でサポートしている機能が違うのがよくわからん・・・）

これも、ネットを色々検索した結果(GPT先生はまともな答えを返さなかった)[gitHubのissue](https://github.com/mockk/mockk/issues/1035)に解決方法がありました。
**mockk-1.13.4、1.13.5で出る特有の問題のようです。1.13.3に下げることで解消しました。**

どうも、mockkはInstrument testをサポートしいるものの、バージョンには敏感なようです(他のライブラリのバージョンとも関連しているのか？)。これら事象は将来のバージョンで解消されるかもしれません。
```gradle:build.gradle
dependencies {
    ・・・
    androidTestImplementation 'io.mockk:mockk-android:1.13.3'
    androidTestImplementation 'io.mockk:mockk-agent:1.13.3'
}
```
# 具体例、RecyclerViewのswipe
これでやっと、Instrument test開始できるようになりました。
やりたかったテストがRecyclerViewの左右のswipeでItemTouchHelperが呼ばれるのをEspressoでテストしたかったのですが、どうすればいいのか？
```kotlin
val ith = ItemTouchHelper(
    object : ItemTouchHelper.SimpleCallback(
        ItemTouchHelper.UP or ItemTouchHelper.DOWN,
        ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
    ){
        override fun onMove(
            recyclerView: RecyclerView,
            viewHolder: RecyclerView.ViewHolder,
            target: RecyclerView.ViewHolder
        ): Boolean {
            return false // 移動はさせない
        }
        override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
            // ★ ここをテストしたい
        }
    })
ith.attachToRecyclerView(recyclerView)
```
RecyclerViewのswipeをテストするためにはespresso-contribを依存関係に追加します。
```gradle:build.gradle
dependencies {
    ・・・
    androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1'
}
```
テストコードは、以下の様になります。
```kotlin
val scenario = launchFragmentInContainer <MyFragment>()
scenario.onFragment { fragment ->
}
// assert
onView(withId(R.id.myView)).check(
    ViewAssertions.matches(ViewMatchers.hasChildCount(2))
)
// 左にswipe
onView(withId(R.id.materialView)).perform(
    RecyclerViewActions.actionOnItemAtPosition<MyAdapter.ViewHolder>(0, swipeLeft())
)
// assert
onView(withId(R.id.materialView)).check(
    ViewAssertions.matches(ViewMatchers.hasChildCount(1)) // ★ FIXME 必ずしも1にならない
)
```
最初にRecyclerViewに2件あることを確認して、左にswipe、1件に減っているはずですが、★で、必ずしも1にはなりません。
それは、Espressoは画面のViewを操作(左にswipe)しているだけで、RecuclerViewの件数はRecyclerView.Adapterが制御しています。
通常は、左にswipeしたことによりその裏の処理、ItemTouchHelper.SimpleCallback#onSwiped でRecyclerView.Adapterの件数から1件削除する処理があるはずです。ここをmock化してしまったら削除されないので、件数は変わりません。

mockkの書き方によってはそこは0番目を削除したように振る舞うことができますが、そうでない場合は注意が必要です。