概要
以下のような、シンプルな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で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と置き換えてみました。
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)の組み合わせはうまくいかない