如何确保在 Android 单元测试中调用 ViewModel#onCleared?

How to ensure ViewModel#onCleared is called in an Android unit test?

这是我的 MWE 测试 class,它依赖于 AndroidX、JUnit 4 和 MockK 1.9:

class ViewModelOnClearedTest {
    @Test
    fun `MyViewModel#onCleared calls Object#function`() = mockkObject(Object) {
        MyViewModel::class.members
            .single { it.name == "onCleared" }
            .apply { isAccessible = true }
            .call(MyViewModel())

        verify { Object.function() }
    }
}

class MyViewModel : ViewModel() {
    override fun onCleared() = Object.function()
}

object Object {
    fun function() {}
}

注意:该方法在superclass ViewModel.

中被保护

我想验证 MyViewModel#onCleared 调用了 Object#function。上面的代码通过反射完成了这个。我的问题是:我能否以某种方式 运行 或模拟 Android 系统以便调用 onCleared 方法,这样我就不需要反射?

来自 onCleared JavaDoc:

This method will be called when this ViewModel is no longer used and will be destroyed.

所以,换句话说,我如何创建这种情况,以便我知道 onCleared 被调用并且我可以验证其行为?

TL;DR

在此答案中,Robolectric 用于让 Android 框架在您的 ViewModel 上调用 onCleared。这种测试方式比使用反射(如问题)慢,并且取决于 Robolectric 和 Android 框架。取舍取决于您。


正在查看 Android 的来源...

...你可以看到 ViewModel#onCleared 只在 ViewModelStore 中被调用(对于你自己的 ViewModels)。这是视图模型的存储 class,由 ViewModelStoreOwner classes 拥有,例如FragmentActivity。那么,ViewModelStore 什么时候在您的 ViewModel 上调用 onCleared

它必须存储你的ViewModel,然后必须清除存储(你不能自己做)。

当您使用 ViewModelProviders.of(FragmentActivity activity).get(Class<T> modelClass) get 您的 ViewModel 时,您的视图模型由 ViewModelProvider 存储,其中 T 是您的视图模型 class.它存储在 FragmentActivityViewModelStore 中。

例如,当您的片段 activity 被销毁时,商店就会清空。这是一堆到处都是的链式调用,但基本上是:

  1. 有一个FragmentActivity.
  2. 使用 ViewModelProviders#of 获取它的 ViewModelProvider
  3. 使用 ViewModelProvider#get 获取您的 ViewModel
  4. 摧毁你的activity。

现在,应该在您的视图模型上调用 onCleared。让我们使用 Robolectric 4、JUnit 4、MockK 1.9 对其进行测试:

  1. @RunWith(RobolectricTestRunner::class) 添加到您的测试中 class。
  2. 使用 Robolectric.buildActivity(FragmentActivity::class.java)
  3. 创建一个 activity 控制器
  4. 在控制器上使用 setup 初始化 activity,这允许它被销毁。
  5. 通过控制器的get方法获取activity。
  6. 按照上述步骤获取视图模型。
  7. 使用控制器上的 destroy 摧毁 activity。
  8. 验证 onCleared 的行为。

完整示例class...

...基于问题的示例:

@RunWith(RobolectricTestRunner::class)
class ViewModelOnClearedTest {
    @Test
    fun `MyViewModel#onCleared calls Object#function`() = mockkObject(Object) {
        val controller = Robolectric.buildActivity(FragmentActivity::class.java).setup()

        ViewModelProviders.of(controller.get()).get(MyViewModel::class.java)

        controller.destroy()

        verify { Object.function() }
    }
}

class MyViewModel : ViewModel() {
    override fun onCleared() = Object.function()
}

object Object {
    fun function() {}
}

我刚刚为 ViewModel 创建了这个扩展:

/**
 * Will create new [ViewModelStore], add view model into it using [ViewModelProvider]
 * and then call [ViewModelStore.clear], that will cause [ViewModel.onCleared] to be called
 */
fun ViewModel.callOnCleared() {
    val viewModelStore = ViewModelStore()
    val viewModelProvider = ViewModelProvider(viewModelStore, object : ViewModelProvider.Factory {

        @Suppress("UNCHECKED_CAST")
        override fun <T : ViewModel?> create(modelClass: Class<T>): T = this@callOnCleared as T
    })
    viewModelProvider.get(this@callOnCleared::class.java)

    //Run 2
    viewModelStore.clear()//To call clear() in ViewModel
}

在 kotlin 中,您可以使用 public 覆盖受保护的可见性,然后从测试中调用它。

class MyViewModel: ViewModel() {
    public override fun onCleared() {
        ///...
    }
}

对于 Java,如果您在与 ViewModel class(此处为 MyViewModel)相同的包(在测试目录中)中创建测试 class,那么您可以从测试 class 中调用 onCleared 方法;因为受保护的方法也是包私有的。