单元测试作用域视图模型的最佳方法

Best approach for unit-testing scoped viewmodels

在处理 viewModel 内部的协同程序时,最好使用 viewModel 实现 CoroutineScope,以便在清除 viewModel 时取消所有协同程序。通常我看到 coroutineContext 被定义为 Dispatchers.Main + _job 这样协程默认在主 UI 线程中执行。通常这是在一个开放的 class 上完成的,这样你所有的 viewModels 都可以扩展它并在没有样板代码的情况下获得范围。

当尝试对所述 viewModels 进行单元测试时出现问题,因为 Dispatchers.Main 不可用并且尝试使用它会引发异常。我正在尝试找到一个好的解决方案,该解决方案不涉及外部库或子视图模型上的太多样板。

我目前的解决方案是将主上下文添加为构造函数参数,并将 Dispatchers.Main 作为默认值。然后在单元测试中,在测试 viewModel 之前我将它设置为 Dispatchers.Default。我不喜欢这个解决方案,因为它公开了 coroutineContext 实现细节供所有人查看和更改:

open class ScopedViewModel(var maincontext = Dispatchers.Main) : ViewModel(), CoroutineScope {
    private val _job = Job()
    override val coroutineContext: CoroutineContext
        get() = maincontext + _job

    override fun onCleared() {
        super.onCleared()
        _job.cancel()
    }
}
class MyViewModel : ScopedViewModel() {}

在测试中:

fun setup(){
    viewModel = MyViewModel()
    viewModel.maincontext = Dispacther.Default
}

我个人从 RxJava2 复制了一个解决方案:如果你的测试 运行s 针对 RxJava2 流程,其中包括两个或更多不同的调度程序,你当然希望所有这些 运行 在一个线程。 下面是 RxJava2 测试是如何完成的:

@BeforeClass
public static void prepare() {
    RxJavaPlugins.setComputationSchedulerHandler(scheduler -> Schedulers.trampoline());
    RxJavaPlugins.setIoSchedulerHandler(scheduler -> Schedulers.trampoline());
    RxJavaPlugins.setNewThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
    RxJavaPlugins.setSingleSchedulerHandler(scheduler -> Schedulers.trampoline());
    RxAndroidPlugins.setMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
}

我对协程也做了同样的事情。刚刚创建了一个 class 收集调度程序,但这些调度程序可以更改。

object ConfigurableDispatchers {

@JvmStatic
@Volatile
var Default: CoroutineDispatcher = Dispatchers.Default

@JvmStatic
@Volatile
var Main: MainCoroutineDispatcher = Dispatchers.Main

...
}

并且,在 @BeforeClass 方法中我调用

@ExperimentalCoroutinesApi
fun setInstantMainDispatcher() {
    Main = object : MainCoroutineDispatcher() {
        @ExperimentalCoroutinesApi
        override val immediate: MainCoroutineDispatcher
            get() = this

        override fun dispatch(context: CoroutineContext, block: Runnable) {
            block.run()
        }
    }
}

这将保证该块将在调用线程中执行。

这是我发现构造函数注入的唯一替代方法。