概要
以下のような、シンプルなViewModelのテストをMockitoを使って書いたのですが、これをMockKで書き換えることにしました。
Mockitoを使ったViewModelのテスト
@RunWith(RobolectrictTestRunner::class)
class UserViewModelTest {
@Mock private lateinit var mockRepository: MyRepository
@Mock private lateinit var observer: Observer<List<UserPoint>>
private lateinit var viewModel
@Before
fun init() {
val immediate = object : Scheduler() {
override fun createWorker(): Scheduler.Worker {
return ExecutorScheduler.ExecutorWorker(Executor { it.run() })
}
}
RxJavaPlugins.setInitIoSchedulerHandler { _ -> immediate }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { _ -> immediate }
MockitoAnnotations.initMocks(this)
}
@Test
fun someTest() {
val userList = listOf()
`when`(mockRepository.findUserList()).thenReturn(Observable.just(userList))
viewModel = UserViewModel(repository)
viewModel.users.observeForever(observer)
viewModel.loadUserList()
verify(mockRepository, only()).findUserList()
verify(observer).onChanged(userList)
}
}
MockKとは
MockKはKotlinのためのMocking Libraryです。
ドキュメントも充実しているし、KotlintだとMockitoで困ることがほぼ解決出来ます。
便利なポイントはドキュメントにも列挙されています。個人的にはObjectのMockがシュッと書けて好きです。
mockk.io
MockKでViewModelのテスト
前述のコードをMockKで書き直すと以下のようになりました。
MockKを使ったViewModelのテスト
@RunWith(RobolectricTestRunner::class)
class UserViewModelTest {
@MockK
private lateinit var mockRepository: MyRepository
@MockK
private lateinit var observer: Observer<List<User>>
private lateinit var viewModel
@Before
fun setup() {
val immediate = object : Scheduler() {
override fun createWorker(): Scheduler.Worker {
return ExecutorScheduler.ExecutorWorker(Executor { it.run() })
}
}
RxJavaPlugins.setInitIoSchedulerHandler { _ -> immediate }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { _ -> immediate }
MockKAnnotations.init(this)
}
@Test
fun someTest() {
val userList = listOf()
every { mockRepository.findUserList() } returns Observable.just(userList)
viewModel = UserViewModel(mockRepository)
viewModel.users.observeForever(observer)
viewModel.loadUserList()
verify { mockRepository.findUserList(authToken) }
assert(viewModel.users.value == userList)
}
}
ところが、これを実行するとMockKのイニシャライズが失敗し、以下のようなエラーが発生してしまいました。
java.lang.NoClassDefFoundError: io/mockk/proxy/jvm/dispatcher/JvmMockKWeakMap
at io.mockk.proxy.jvm.JvmMockKAgentFactory$init$Initializer.handlerMap(JvmMockKAgentFactory.kt:98)
at io.mockk.proxy.jvm.JvmMockKAgentFactory$init$Initializer.init(JvmMockKAgentFactory.kt:43)
at io.mockk.proxy.jvm.JvmMockKAgentFactory.init(JvmMockKAgentFactory.kt:102)
at io.mockk.impl.JvmMockKGateway.<init>(JvmMockKGateway.kt:45)
at io.mockk.impl.JvmMockKGateway.<clinit>(JvmMockKGateway.kt:163)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
...
Caused by: java.lang.ClassNotFoundException: io.mockk.proxy.jvm.dispatcher.JvmMockKWeakMap
at org.robolectric.internal.bytecode.InstrumentingClassLoader.getByteCode(InstrumentingClassLoader.java:168)
at org.robolectric.internal.bytecode.InstrumentingClassLoader.findClass(InstrumentingClassLoader.java:123)
at org.robolectric.internal.bytecode.InstrumentingClassLoader.loadClass(InstrumentingClassLoader.java:95)
... 34 more
何が原因か分からなかったのですが、そもそもRobolectrictTestRunnerを使っている理由は、テストにLiveDataが含まれておりvalueの更新処理をMockしてもらうためです。
調べてみると、下記記事のようにLiveDataのユニットテストは core-testing
ライブラリを使うのが正攻法のようなので、Robolectrictと置き換えてみました。
medium.com
MockK&core-testingを使ったViewModelのテスト
class UserViewModelTest {
@Rule
@JvmField
var rule = InstantTaskExecutorRule()
@MockK
private lateinit var mockRepository: MyRepository
@MockK
private lateinit var observer: Observer<List<User>>
private lateinit var viewModel
@Before
fun setup() {
val immediate = object : Scheduler() {
override fun createWorker(): Scheduler.Worker {
return ExecutorScheduler.ExecutorWorker(Executor { it.run() })
}
}
RxJavaPlugins.setInitIoSchedulerHandler { _ -> immediate }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { _ -> immediate }
MockKAnnotations.init(this)
}
@Test
fun someTest() {
val userList = listOf()
every { mockRepository.findUserList() } returns Observable.just(userList)
viewModel = UserViewModel(mockRepository)
viewModel.users.observeForever(observer)
viewModel.loadUserList()
verify { mockRepository.findUserList(authToken) }
assert(viewModel.users.value == userList)
}
}
変更したのは、 RobolectrictTestRunner
を外しRuleに InstantTaskExecutorRule
を設定したことです。
これでやっとうまくテストをパス出来ました。
まとめ
- LiveDataのユニットテストは
core-testing
を使う
- 今の所MockK(v1.8.9)とRobolectrict(v3.0.0)の組み合わせはうまくいかない