EDIT MODE

モバイルアプリ開発に関することを書いています

MockKを使ったViewModelのテスト

概要

以下のような、シンプルな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)の組み合わせはうまくいかない