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

Eri*_*rik 9 android unit-testing android-testing android-viewmodel android-architecture-components

这是我的 MWE 测试类,它依赖于 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() {}
}
Run Code Online (Sandbox Code Playgroud)

注意:该方法在超类中受保护ViewModel

我想验证MyViewModel#onCleared调用Object#function. 上面的代码通过反射实现了这一点。我的问题是:我可以以某种方式运行或模拟 Android 系统以便onCleared调用该方法,这样我就不需要反射吗?

来自onClearedJavaDoc:

当这个 ViewModel 不再使用时会调用这个方法,并且会被销毁。

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

小智 13

我刚刚创建了 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
}
Run Code Online (Sandbox Code Playgroud)


mig*_*uel 8

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

class MyViewModel: ViewModel() {
    public override fun onCleared() {
        ///...
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 在 `onCleared()` 之上加上一点 `@RestrictTo(RestrictTo.Scope.TESTS)` 就可以了 (5认同)
  • 与 androidx.annotation: ```@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) public override fun onCleared() {...``` (2认同)

Eri*_*rik 5

TL; 博士

在这个答案中,Robolectric 用于让 Android 框架onCleared在您的ViewModel. 这种测试方式比使用反射慢(如问题中所示),并且取决于 Robolectric 和 Android 框架。这种权衡取决于你。


查看Android的源...

...你可以看到ViewModel#onCleared它只被调用ViewModelStore(为你自己的ViewModels)。这是视图模型的存储类,由ViewModelStoreOwner类拥有,例如FragmentActivity. 那么,什么时候ViewModelStore调用onCleared你的ViewModel?

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

您的视图模型由您使用ViewModelProvider时存储,您的视图模型类在哪里。它存储在的。getViewModelViewModelProviders.of(FragmentActivity activity).get(Class<T> modelClass)TViewModelStoreFragmentActivity

例如,当您的 Fragment 活动被销毁时,商店就很清楚了。这是一堆遍布各处的连锁调用,但基本上是:

  1. 有一个FragmentActivity
  2. 得到它的ViewModelProvider使用ViewModelProviders#of
  3. 得到你的ViewModel使用ViewModelProvider#get
  4. 破坏你的活动。

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

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

完整示例类...

...基于问题的例子:

@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() {}
}
Run Code Online (Sandbox Code Playgroud)