AndroidのJUnitテストは超ムズい、EspressoでInstrument test
AndroidのJUnitテストは超ムズい
AndroidのJUnitテストは超ムズい(2)
の続編です。
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にありました。
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をサポートしています。
(Android P未満とP以上でサポートしている機能が違うのがよくわからん・・・)
これも、ネットを色々検索した結果(GPT先生はまともな答えを返さなかった)gitHubのissueに解決方法がありました。
mockk-1.13.4、1.13.5で出る特有の問題のようです。1.13.3に下げることで解消しました。
どうも、mockkはInstrument testをサポートしいるものの、バージョンには敏感なようです(他のライブラリのバージョンとも関連しているのか?)。これら事象は将来のバージョンで解消されるかもしれません。
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でテストしたかったのですが、どうすればいいのか?
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を依存関係に追加します。
dependencies {
・・・
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1'
}
テストコードは、以下の様になります。
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番目を削除したように振る舞うことができますが、そうでない場合は注意が必要です。